This is a hidden blade inside the admin toolbox, not a lockpick on the front door
CVE-2025-53870 is an OS command injection in the FortiAP/FortiAP-W2 CLI that lets an authenticated attacker run unauthorized commands by sending a crafted CLI command. Affected ranges published on May 12, 2026 are FortiAP 7.6.0-7.6.2, 7.4.0-7.4.5, all 7.2.x, all 7.0.x, all 6.4.x, plus FortiAP-W2 7.4.0-7.4.4, 7.2.0-7.2.5, and all 7.0.x.
Fortinet's medium label is technically fair in a lab, but too hot for enterprise prioritization. The decisive friction is not payload complexity; it's attacker position: AV:L + PR:H means the operator already has privileged access to the AP management plane or a controller-mediated CLI path, which is a post-initial-access condition with a narrow exposed population.
4 steps from start to impact.
Reach an AP management path with OpenSSH or FortiGate diag w-c wlac wtpcmd
- Access to the AP management network or to the FortiGate acting as wireless controller
- FortiAP/FortiAP-W2 deployed on an affected firmware branch
- A reachable CLI path exists
- Fortinet notes FortiAP is often behind NAT and SSH to the AP is frequently unavailable
- Many enterprises manage APs centrally through FortiGate instead of exposing AP admin directly
- This is not an internet-reachable unauthenticated path
Obtain privileged AP credentials via TACACS or local admin
- Valid authenticated access
- Privilege level high enough to use the vulnerable CLI path
- This implies the attacker is already in your admin plane
- AAA, MFA on upstream admin access, and role separation reduce the reachable user pool
- Wireless admins are usually a small subset of IT, not every domain user
Send the crafted CLI payload to the vulnerable parser
- Affected FortiAP/FortiAP-W2 version
- Authenticated privileged CLI session
- No public PoC was readily discoverable at assessment time
- Appliance CLI exploitation is harder to automate at scale than web RCE
- Version sprawl matters: patched 7.6.3/7.4.6/7.4.5/7.2.6 branches break the chain
Execute commands on the AP and abuse the local blast radius
- Successful command injection
- Operational value in the target AP's location or network segment
- APs are constrained appliances with limited downstream trust
- Most enterprise damage still requires a second step beyond the AP itself
- Fleet-wide impact depends on separate controller/admin compromise, not this bug alone
The supporting signals.
| In-the-wild status | No active exploitation evidence found. Fortinet PSIRT marks Known Exploited: No, and the CVE is not present in the CISA KEV catalog as checked against the public catalog. |
|---|---|
| Public PoC status | No credible public PoC or weaponized GitHub repo was found during this review. That matches the current low-heat profile: more likely hand-operated by a privileged insider or post-compromise operator than mass exploitation. |
| EPSS | 0.00042 from the user intel block, which is roughly 0.042% likelihood in the EPSS model. That is extremely low and directionally consistent with the attacker-position friction. |
| KEV | Not KEV-listed as of this assessment. No CISA remediation date is attached because it is absent from KEV. |
| CVSS reality check | The vector CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H says the quiet part out loud: local/admin-plane access first. This is not a front-door internet bug; it is privileged abuse after the attacker already has footing. |
| Affected versions | FortiAP 7.6.0-7.6.2, 7.4.0-7.4.5, all 7.2.x, all 7.0.x, all 6.4.x; FortiAP-W2 7.4.0-7.4.4, 7.2.0-7.2.5, all 7.0.x. |
| Fixed versions | FortiAP: 7.6.3+, 7.4.6+; FortiAP-W2: 7.4.5+, 7.2.6+. Older FortiAP 7.2.x, 7.0.x, 6.4.x are not fixed in-branch per the advisory and must migrate to a fixed release. |
| Exposure population | Fortinet documents that FortiAP is often behind NAT and SSH to the AP is often unavailable, while FortiGate can proxy shell commands over CAPWAP. Translation: reachable population is usually internal management plane, not internet-scale. |
| Mgmt-surface details | FortiAP CLI docs show ALLOW_SSH can be disabled or controller-managed, and Fortinet added remote TACACS access for AP management in 7.6.1. That means exposure depends heavily on your admin design, not just firmware version. |
| Disclosure and researcher | Published 2026-05-12. Fortinet credits Gwendal Guégniaud of Fortinet Product Security and marks the issue as internally discovered. |
noisgate verdict.
The single biggest downgrade driver is the attacker position requirement: this bug needs already-authenticated, high-privilege access to the FortiAP admin plane or to a controller path that can issue AP CLI commands. That makes it a post-compromise abuse path on edge infrastructure, not a realistic enterprise-wide initial-access event.
Why this verdict
- Start from vendor medium because successful exploitation does produce real command execution on infrastructure gear, not a cosmetic bug.
- Downward adjustment: attacker position is already bad —
AV:LandPR:Hmean the operator already has privileged access to the AP or controller admin path, which implies prior compromise, insider access, or stolen admin credentials. - Downward adjustment: exposure population is narrow — FortiAPs are commonly behind NAT, SSH may be unavailable or controller-controlled, and many enterprises never expose AP administration directly.
- Downward adjustment: no field heat — no KEV listing, no public exploitation evidence, and an extremely low EPSS score all argue against emergency prioritization.
Why not higher?
There is no unauthenticated remote path and no evidence this can be sprayed across the internet. Even when exploited, the immediate blast radius is usually one AP or one site edge device unless the attacker separately owns broader controller or admin credentials.
Why not lower?
This is still command execution on wireless infrastructure, not a harmless misconfiguration. Large affected version ranges and controller-mediated management paths mean exposed fleets do exist, so defenders should still inventory and remediate it as part of normal firmware hygiene.
What to do — in priority order.
- Shut off direct AP admin where you do not need it — Disable or tightly restrict direct SSH/HTTPS management to FortiAP units and force administration through the controller where possible. For a LOW finding there is no formal mitigation deadline, so treat this as backlog hygiene and fold it into your next wireless-management hardening cycle.
- Constrain the management plane — Limit AP management to a dedicated admin VLAN or jump host set, and do not allow broad east-west access to AP interfaces. This directly attacks the first prerequisite in the chain and should be implemented during normal network hardening, not as an outage-driven emergency.
- Tighten admin auth on FortiGate and AP workflows — Review TACACS/local admin assignments, remove stale AP admin roles, and ensure privileged wireless administration is narrow and monitored. For a LOW verdict, do this in the next identity and access review cycle rather than inventing an emergency clock.
- Monitor AP admin events — Forward FortiGate and AP admin logs into SIEM and alert on unusual AP SSH/TACACS logins, off-hours management, and rare controller-to-AP shell activity. Detection is more valuable here than perimeter blocking because the exploit path lives in the admin plane.
- A WAF does not help because this is not a web application exploit path.
- Internet-facing IPS signatures are weak compensation because many affected APs are not directly reachable and the vulnerable surface is CLI/admin-plane traffic.
- Endpoint EDR on user laptops does not materially reduce risk on the appliance itself unless those laptops are the admin jump hosts being used to manage APs.
Crowdsourced verification payload.
Run this on an auditor workstation or CI job, not on the AP itself. Export a CSV from FortiGate inventory or your CMDB with columns product,version[,asset], then run python3 check_cve_2025_53870.py fortiap_inventory.csv; no special privileges are needed if you already have the inventory export.
#!/usr/bin/env python3
# check_cve_2025_53870.py
# Inventory-based verifier for CVE-2025-53870
# Input CSV columns: product,version[,asset]
# Output per row: VULNERABLE / PATCHED / UNKNOWN
# Exit codes:
# 0 = all checked rows PATCHED
# 1 = one or more VULNERABLE rows found
# 2 = no vulnerable rows, but one or more UNKNOWN rows
# 3 = usage / file / parse error
import csv
import re
import sys
from pathlib import Path
def parse_version(v):
if v is None:
return None
v = v.strip()
m = re.match(r'^(\d+)\.(\d+)(?:\.(\d+))?', v)
if not m:
return None
major = int(m.group(1))
minor = int(m.group(2))
patch = int(m.group(3) or 0)
return (major, minor, patch)
def normalize_product(p):
if p is None:
return None
p = p.strip().lower().replace('_', '-').replace(' ', '')
if p in ('fortiap', 'fap'):
return 'fortiap'
if p in ('fortiap-w2', 'fortiapw2', 'fap-w2', 'fapw2'):
return 'fortiap-w2'
return None
def cmp_ver(a, b):
return (a > b) - (a < b)
def assess(product, version):
vp = parse_version(version)
pr = normalize_product(product)
if pr is None or vp is None:
return ('UNKNOWN', 'unrecognized product or version format')
major, minor, patch = vp
if pr == 'fortiap':
if (major, minor) == (7, 6):
if cmp_ver(vp, (7, 6, 0)) >= 0 and cmp_ver(vp, (7, 6, 3)) < 0:
return ('VULNERABLE', 'FortiAP 7.6.0 through 7.6.2 affected; fixed in 7.6.3+')
if cmp_ver(vp, (7, 6, 3)) >= 0:
return ('PATCHED', 'FortiAP 7.6.3+ not affected by this advisory')
if (major, minor) == (7, 4):
if cmp_ver(vp, (7, 4, 0)) >= 0 and cmp_ver(vp, (7, 4, 6)) < 0:
return ('VULNERABLE', 'FortiAP 7.4.0 through 7.4.5 affected; fixed in 7.4.6+')
if cmp_ver(vp, (7, 4, 6)) >= 0:
return ('PATCHED', 'FortiAP 7.4.6+ not affected by this advisory')
if (major, minor) == (7, 2):
return ('VULNERABLE', 'All FortiAP 7.2.x versions affected; migrate to a fixed release')
if (major, minor) == (7, 0):
return ('VULNERABLE', 'All FortiAP 7.0.x versions affected; migrate to a fixed release')
if (major, minor) == (6, 4):
return ('VULNERABLE', 'All FortiAP 6.4.x versions affected; migrate to a fixed release')
return ('UNKNOWN', 'Version branch not listed in the advisory')
if pr == 'fortiap-w2':
if (major, minor) == (7, 4):
if cmp_ver(vp, (7, 4, 0)) >= 0 and cmp_ver(vp, (7, 4, 5)) < 0:
return ('VULNERABLE', 'FortiAP-W2 7.4.0 through 7.4.4 affected; fixed in 7.4.5+')
if cmp_ver(vp, (7, 4, 5)) >= 0:
return ('PATCHED', 'FortiAP-W2 7.4.5+ not affected by this advisory')
if (major, minor) == (7, 2):
if cmp_ver(vp, (7, 2, 0)) >= 0 and cmp_ver(vp, (7, 2, 6)) < 0:
return ('VULNERABLE', 'FortiAP-W2 7.2.0 through 7.2.5 affected; fixed in 7.2.6+')
if cmp_ver(vp, (7, 2, 6)) >= 0:
return ('PATCHED', 'FortiAP-W2 7.2.6+ not affected by this advisory')
if (major, minor) == (7, 0):
return ('VULNERABLE', 'All FortiAP-W2 7.0.x versions affected; migrate to a fixed release')
return ('UNKNOWN', 'Version branch not listed in the advisory')
return ('UNKNOWN', 'Unhandled product family')
def main():
if len(sys.argv) != 2:
print('UNKNOWN - usage: python3 check_cve_2025_53870.py <inventory.csv>')
sys.exit(3)
path = Path(sys.argv[1])
if not path.is_file():
print(f'UNKNOWN - file not found: {path}')
sys.exit(3)
vulnerable = 0
unknown = 0
checked = 0
try:
with path.open(newline='', encoding='utf-8-sig') as fh:
reader = csv.DictReader(fh)
required = {'product', 'version'}
if not required.issubset(set(reader.fieldnames or [])):
print('UNKNOWN - CSV must contain at least: product,version[,asset]')
sys.exit(3)
for row in reader:
product = row.get('product', '')
version = row.get('version', '')
asset = row.get('asset', '').strip() or '(no-asset-id)'
status, reason = assess(product, version)
checked += 1
if status == 'VULNERABLE':
vulnerable += 1
elif status == 'UNKNOWN':
unknown += 1
print(f'{status},{asset},{product},{version},{reason}')
except Exception as exc:
print(f'UNKNOWN - failed to read CSV: {exc}')
sys.exit(3)
if checked == 0:
print('UNKNOWN - no rows processed')
sys.exit(2)
if vulnerable > 0:
print(f'VULNERABLE - {vulnerable} of {checked} rows affected')
sys.exit(1)
if unknown > 0:
print(f'UNKNOWN - 0 vulnerable rows found, but {unknown} of {checked} rows need manual review')
sys.exit(2)
print(f'PATCHED - all {checked} checked rows are on non-affected versions for this advisory')
sys.exit(0)
if __name__ == '__main__':
main()
If you remember one thing.
7.6.0-7.6.2, 7.4.0-7.4.5, 7.2.x, 7.0.x, 6.4.x, or the matching FortiAP-W2 ranges, and fold them into your normal wireless firmware plan while disabling unnecessary direct AP admin access; for a LOW verdict there is no noisgate mitigation SLA and no noisgate remediation SLA — treat as backlog hygiene, with the special case that legacy 7.0/6.4 FortiAP branches should be documented for migration because the vendor advisory does not provide an in-branch fix.Sources
- NVD record
- Fortinet PSIRT advisory FG-IR-26-133
- FortiAP 7.6.3 release notes PDF
- FortiAP 7.4.6 resolved issues
- FortiAP shell command via FortiGate CAPWAP
- FortiAP CLI configuration and diagnostics commands
- FortiGate support for remote TACACS access to FortiAP 7.6.1
- CISA Known Exploited Vulnerabilities catalog
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.