A 17-year-old booby trap that only fires if you flip two switches nobody uses anymore
CVE-2026-97205 is a use-after-free in NGINX's ngx_http_core_module that triggers when both the aio (asynchronous file I/O) directive and SSI (ssi on) are enabled and a page containing <!--#include virtual="..." --> is processed. The freed memory gets reused by the worker, leading to corruption and a crash. If ASLR is disabled — which it isn't on any modern Linux kernel — the corruption is theoretically chainable to code execution. The bug traces back to NGINX 0.8.11 (circa 2009) and affects every version through 1.31.6 / 1.30.5 on the OSS side, plus NGINX Plus R33–R36 and 37.0.0–37.0.4. Fixed in OSS 1.30.6 / 1.31.7 and Plus 37.0.5.1 / 37.1.0.1. NGINX Ingress Controller, Gateway Fabric, Instance Manager, and F5 WAF for NGINX also ship updated builds.
F5 rates this HIGH (CVSS v3.1 8.1) and even CRITICAL under CVSS v4.0 (9.2), but NGINX's own security advisories page rates it Medium — and NGINX is right. The v3.1 vector sets Attack Complexity to High, acknowledging the non-trivial prerequisites, yet the score still lands at 8.1 because CVSS max-impact triplets (C:H/I:H/A:H) inflate the result. In practice, aio is off by default and requires the --with-file-aio compile-time flag; SSI is a legacy content-composition technique from the 1990s that's off by default and virtually unused in modern reverse-proxy or Kubernetes-ingress deployments. The intersection of both being active on a public-facing NGINX instance is vanishingly small. The RCE angle requires ASLR to be disabled, which has been a non-default state on Linux since 2005. The realistic impact is a DoS via worker crash — and NGINX workers auto-restart, limiting the blast.
5 steps from start to impact.
Identify an NGINX instance
Server: nginx header is returned by default. Banner identification is trivial. No tooling beyond nmap or httpx is needed.- Target runs NGINX in the affected version range (0.8.11–1.31.6)
- Version alone is necessary but not sufficient — two config-level prerequisites remain
Confirm AIO is compiled and enabled
--with-file-aio (not a default build flag) and the operator must have added aio on; or aio threads; to an http, server, or location block. Without this, the aio directive is unknown and NGINX refuses to start. The attacker cannot detect this remotely — it requires config knowledge or blind exploitation.- NGINX compiled with
--with-file-aio - Config contains
aio onoraio threads
- The
aiodirective is off by default. The compile-time flag is omitted in many distro-packaged builds. Operators must deliberately enable it for performance tuning of static file serving. - Attacker has no remote fingerprint for AIO status — this is blind.
nginx -V 2>&1 | grep file-aio and nginx -T | grep -E '^\s*aio'.Confirm SSI is enabled and reachable
ssi on; in a server or location block, and the attacker must be able to request a resource that is processed through the SSI filter — typically static .shtml files or HTML with SSI directives. SSI is a legacy content-inclusion mechanism largely replaced by application-level templating. The attacker needs a URL path that triggers SSI processing containing an include virtual directive.- Config contains
ssi onin a reachable location - A served resource contains
<!--#include virtual="..." -->
- SSI is off by default. Modern NGINX deployments (reverse proxy, API gateway, Kubernetes ingress) do not enable SSI because they proxy requests to backends rather than serving SSI-processed static content.
- The attacker must find or cause a request to an SSI-enabled path with
include virtual— not just any path on the server.
nginx -T | grep -E '^\s*ssi\s+on'. Grep served content for SSI directives.Trigger the use-after-free
include virtual sub-request, the AIO completion callback frees a buffer that the SSI filter still references. The worker accesses freed memory, causing corruption or a segfault. No special payload crafting beyond a normal HTTP GET is needed — the race condition is inherent in the code path.- Steps 2 and 3 both satisfied on the same server/location block
- The compound probability of both AIO and SSI being active on the same location is extremely low.
- Worker crashes are logged and auto-restarted, limiting sustained DoS impact.
worker process exited on signal 11 (SIGSEGV). Core dumps will reference ngx_http_core_module.(Theoretical) Achieve RCE if ASLR is disabled
/proc/sys/kernel/randomize_va_space = 0), the attacker could theoretically leverage the UAF to control a freed heap object and redirect execution. This requires stable heap layout knowledge and is not weaponizable against ASLR-enabled systems. No public exploit achieves RCE.- ASLR disabled on the target system
- Attacker has heap layout knowledge for the specific NGINX build
- ASLR has been enabled by default on Linux since kernel 2.6.12 (2005). Disabling it requires explicit root action (
sysctl kernel.randomize_va_space=0). This condition is virtually non-existent in production. - No public weaponized exploit exists for this UAF-to-RCE chain.
cat /proc/sys/kernel/randomize_va_space (should be 2).ssi on; directive across all server and location blocks, then nginx -t && nginx -s reload. This eliminates the trigger path entirely — no SSI processing means the UAF code path is never reached. If SSI is genuinely needed, migrate to application-layer templating (Jinja2, Handlebars, etc.). This is the highest-leverage control because SSI is the rarer and more replaceable prerequisite. As a MEDIUM-severity finding with no mitigation SLA, go straight to the 365-day remediation window, but this config change can be deployed in minutes and should be prioritized in any audit pass.aio off; in all relevant blocks (or remove the directive entirely — off is the default) and reload. If rebuilding NGINX, omit --with-file-aio. This breaks the other half of the prerequisite chain. Note that disabling AIO may have a minor performance impact on high-throughput static file serving workloads, so test under load before deploying fleet-wide. Deploy within the 365-day remediation window per noisgate SLA for MEDIUM findings./proc/sys/kernel/randomize_va_space is set to 2 (full randomization) across your fleet. This blocks the theoretical RCE escalation even if the UAF fires. This should already be true on any modern Linux, but a compliance scan validates it. Use your configuration management (Ansible, Chef, Puppet) to enforce kernel.randomize_va_space = 2 in /etc/sysctl.conf.apt-cache policy nginx for updates. The noisgate remediation SLA for MEDIUM is 365 days; given the low exploitability, schedule this in your next regular patching cycle.- WAF rules — The trigger is a normal HTTP GET to an SSI-enabled path. There is no malicious payload signature to match; the vulnerability is in NGINX's internal SSI+AIO code path, not in request content. A WAF sitting in front of or on NGINX cannot distinguish a triggering request from a legitimate one.
- Rate limiting — A single well-placed request is sufficient to crash the worker. Rate limiting slows brute-force but does not prevent the initial trigger.
- NGINX worker process limits (
worker_processes,worker_rlimit_nofile) — These control concurrency, not memory safety. They don't prevent the UAF from firing; they only affect how many workers can crash simultaneously.
The supporting signals.
| In-the-Wild Exploitation | None observed. Not listed in CISA KEV. No confirmed campaigns. F5, BleepingComputer, and eSecurity Planet all report no active exploitation as of 2026-10-20. |
|---|---|
| Proof of Concept | A PoC reference was tracked on ~Oct 15, 2026, but no weaponized exploit code was found on GitHub, pocindex.io, ExploitDB, or Nuclei templates. No repository named CVE-2026-97205 exists on GitHub. The researcher Cyberlanc3r is credited with discovery but has not published an exploit. |
| EPSS | 0.00484 (0.484%), ~37th percentile as of 2026-10-16. This places it well below the exploitation-likely threshold, consistent with the non-default config requirements. |
| CISA KEV Status | Not listed. No due date set. |
| CVSS v3.1 Vector | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — Network-accessible, unauthenticated, no user interaction, but Attack Complexity: High. The max-impact triplet (C/I/A all High) inflates the score; the AC:H correctly signals that exploitation is not straightforward. |
| CVSS v4.0 | 9.2 (Critical) per F5. NGINX project's own advisories rate this Medium — a significant discrepancy that favors the NGINX assessment given the non-default prerequisites. |
| Affected Versions | NGINX OSS 0.8.11–1.31.6 (stable branch: 1.0.0–1.30.5). NGINX Plus R33–R36, 37.0.0–37.0.4. Also: Ingress Controller <5.7.0, Gateway Fabric <2.8.0, Instance Manager <2.24.0, F5 WAF for NGINX <5.16.0. |
| Fixed Versions | NGINX OSS 1.30.6 (stable), 1.31.7 (mainline). NGINX Plus 37.0.5.1, 37.1.0.1. Distro backports: Debian unstable has the fix; Ubuntu marked 'needs evaluation'; RHEL status pending. |
| Shodan / Internet Exposure | Shodan indexes ~58.5 million NGINX instances globally. However, the vulnerable configuration subset (AIO + SSI both enabled) is undetectable remotely and estimated at well under 1% of the installed base. SSI is a legacy feature; AIO requires explicit compile-time and runtime enablement. |
| Researcher / Reporter | Cyberlanc3r (credited by F5). Disclosed as part of a 5-CVE batch on 2026-10-14 alongside CVE-2026-15176, CVE-2026-93479, CVE-2026-95101, and CVE-2026-97629. |
Sources.
- F5 Security Advisory K000162608
- NGINX Official Security Advisories
- SecurityOnline — F5 Patches Three Critical NGINX Vulnerabilities
- BleepingComputer — F5 Releases Out-of-Band Patches for NGINX
- Ionix Threat Center — CVE-2026-97205
- Upwind — CVE-2026-97205 Analysis
- NGINX ngx_http_core_module Documentation (aio directive)
- Debian Security Tracker — nginx
Why this verdict
- Dual non-default prerequisites kill reachability. Both
aio(off by default, requires compile-time flag) andssi on(off by default, legacy tech) must be active on the same location block. Each alone is uncommon; the intersection is vanishingly rare. This is the single largest downward pressure — the F5 CVSS v3.1 AC:H acknowledges complexity but cannot model the compound configuration requirement. - RCE is theoretical, not practical. The UAF's RCE path requires ASLR to be disabled, which has been the non-default state on Linux since 2005. No public exploit achieves code execution. The realistic ceiling is DoS via worker crash — and workers auto-restart within milliseconds, capping availability impact.
- NGINX's own project rates this Medium. The vendor (F5) assigns 8.1/9.2, but NGINX's security advisories page — the upstream authority — labels it Medium. The discrepancy arises because F5 scores the theoretical maximum impact; NGINX scores the practical exploitability. We side with NGINX's assessment.
- EPSS and threat landscape confirm low urgency. EPSS at 0.484% (37th percentile), no KEV listing, no in-the-wild exploitation, no weaponized PoC. The threat signal is flat one week post-disclosure.
- Role multiplier: network edge. NGINX is canonically a network-edge component (reverse proxy, load balancer, ingress controller) — a high-value role. However, the chain does not succeed in the vast majority of high-value deployments because reverse proxies and ingress controllers do not enable SSI (they forward requests to backends, not process static HTML). The only scenario where AIO+SSI are both active is a legacy static-content server pattern that represents far less than 1% of the NGINX installed base. The blast radius when it does succeed is a worker crash (auto-restarted), not domain takeover, fleet compromise, or supply-chain pivot. The HIGH floor does not apply because neither the ≥1% population threshold nor the catastrophic-outcome threshold is met.
Why not higher?
A HIGH or CRITICAL rating would require either a realistic RCE path or a large exposed population. The RCE path is blocked by ASLR on every modern Linux system, and no exploit bypasses it. The exposed population is gated by two independent non-default configuration switches, both of which are legacy features with negligible adoption in modern NGINX deployment patterns. Even NGINX's upstream project only rates this Medium.
Why not lower?
The bug IS remotely triggerable without authentication, and in the rare deployment that has both AIO and SSI enabled, a single GET request crashes the worker — that's a real, if narrow, DoS vector. The 17-year latency of the bug means some legacy deployments with ossified configs may still carry both directives. A LOW rating would undercount the risk to those long-tail environments. MEDIUM correctly captures 'patch in your normal cycle, but audit your configs now.'
Crowdsourced verification payload.
Run this script on each NGINX host as root (or a user that can execute nginx -V and nginx -T). Invoke with: sudo bash check_cve_2026_97205.sh. It checks the NGINX version, compile-time AIO flag, and runtime AIO+SSI configuration. Exits 0 for PATCHED, 1 for VULNERABLE, 2 for UNKNOWN.
#!/bin/bash
# check_cve_2026_97205.sh — CVE-2026-97205 detector
# Use-after-free in NGINX ngx_http_core_module (AIO + SSI)
# Run as root on the NGINX host.
# Exit codes: 0=PATCHED 1=VULNERABLE 2=UNKNOWN
set -u
if ! command -v nginx &>/dev/null; then
echo "UNKNOWN — nginx binary not found"
exit 2
fi
VER=$(nginx -v 2>&1 | grep -oP 'nginx/\K[\d.]+')
if [ -z "$VER" ]; then
echo "UNKNOWN — could not parse nginx version"
exit 2
fi
echo "[*] nginx version: $VER"
# Version comparison helper (returns 0 if $1 >= $2)
ver_gte() { [ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" = "$2" ]; }
# Determine if version is patched
PATCHED=false
if [[ "$VER" =~ ^1\.30\. ]] && ver_gte "$VER" "1.30.6"; then
PATCHED=true
elif ver_gte "$VER" "1.31.7"; then
PATCHED=true
elif ! ver_gte "$VER" "0.8.11"; then
PATCHED=true # older than affected range
fi
if $PATCHED; then
echo "PATCHED — nginx $VER is not in the affected range (0.8.11–1.31.6)"
exit 0
fi
echo "[!] Version $VER is in affected range"
# Check compile-time AIO support
AIO_BUILT=false
if nginx -V 2>&1 | grep -q -- '--with-file-aio'; then
AIO_BUILT=true
echo "[!] Compiled with --with-file-aio"
else
echo "[+] NOT compiled with --with-file-aio — aio directive unavailable"
fi
# Check runtime config for aio and ssi
CONF=$(nginx -T 2>/dev/null || echo "")
AIO_ON=false
SSI_ON=false
if echo "$CONF" | grep -Pq '^\s*aio\s+(on|threads)'; then
AIO_ON=true
echo "[!] 'aio on/threads' is ENABLED in config"
else
echo "[+] 'aio' is not enabled in config"
fi
if echo "$CONF" | grep -Pq '^\s*ssi\s+on'; then
SSI_ON=true
echo "[!] 'ssi on' is ENABLED in config"
else
echo "[+] 'ssi' is not enabled in config"
fi
# Check ASLR
ASLR=$(cat /proc/sys/kernel/randomize_va_space 2>/dev/null || echo "unknown")
if [ "$ASLR" = "0" ]; then
echo "[!!!] ASLR is DISABLED — RCE risk elevated"
elif [ "$ASLR" != "unknown" ]; then
echo "[+] ASLR enabled (value=$ASLR)"
fi
# Verdict
echo ""
if $AIO_BUILT && $AIO_ON && $SSI_ON; then
echo "VULNERABLE — nginx $VER with AIO and SSI both active. Exploit conditions fully met."
exit 1
elif $AIO_ON && $SSI_ON; then
echo "VULNERABLE — nginx $VER: aio and ssi both in config (aio compile flag unconfirmed)."
exit 1
else
echo "VULNERABLE (version-only) — nginx $VER is in affected range but exploit requires both aio AND ssi enabled. Current config does NOT meet trigger conditions."
exit 1
fi