Safeguard
Vulnerability Analysis•

nginx 1.30.6 and 1.31.7: Five Configuration-Dependent CVEs and How to Check Your Config

nginx 1.30.6 and 1.31.7 fix five CVEs, three scored 9.2 by F5. Each needs a specific configuration to be reachable. What triggers each one, F5's workarounds, and a script that checks your nginx -T output.

Safeguard Security Team
Security Research
10 min read

nginx 1.30.6 (stable) and 1.31.7 (mainline), released on 14 October 2026, fix five vulnerabilities. Three are memory-safety bugs in worker processes: two heap buffer overflows and a use-after-free. F5 scores each of them CVSS 4.0 9.2 and says they could allow code execution where ASLR is disabled or can be bypassed. The other two let an attacker inject data into proxied HTTP/3 requests and forge TLS sessions, which can bypass client certificate checks. nginx.org rates all five as medium severity, because each needs a specific configuration to be reachable.

That configuration dependence is the useful part. This post is for anyone running nginx Open Source or NGINX Plus. It explains what each bug needs, gives F5's workaround for each, and includes a script that reads your nginx -T output and points at the directives worth reviewing.

The short answer

Upgrade to nginx 1.30.6 or 1.31.7, or the matching NGINX Plus release. If you can't upgrade straight away, check your configuration for five patterns: alias with rewrite and $request_filename; regex map captures used in root or log_format; HTTP/3 in front of proxy_pass with request buffering on; a shared TLS session cache without a static ticket key; and aio together with SSI. If none of those apply to you, the practical risk until you upgrade is low.

CVEWhat it isNeedsVulnerable (nginx.org)F5 CVSS 4.0
CVE-2026-15176Heap buffer overflowalias plus rewrite, with $request_filename in set/return, or in log_format0.1.29 to 1.31.69.2
CVE-2026-93479Heap buffer overflow in string expressionsmap with regex, its captures used in root or log_format (http and stream)1.30.0 to 1.30.5, 1.31.2 to 1.31.69.2
CVE-2026-97205Use-after-freeaio enabled and SSI pages with include virtual0.8.11 to 1.31.69.2
CVE-2026-95101Data injection into proxied requestHTTP/3 with proxy_request_buffering on (the default)1.25.0 to 1.31.66.9
CVE-2026-97629TLS session forgeryssl_session_cache shared:... with automatic ticket key rotation1.23.2 to 1.31.68.3

Fixed in 1.30.6 and 1.31.7. The version ranges come from the nginx.org security advisories page. The scores and workarounds come from F5's CVE records.

What each bug needs

CVE-2026-15176: alias after a rewrite. The changelog says a buffer overflow "might occur in a worker process when using the alias directive if the request URI was changed by the rewrite directive and was then mapped to a file path". F5's record names the trigger: a location using alias with rewrite, plus a set or return that references $request_filename, or $request_filename in an access log format. F5's workaround is to avoid alias in such locations, or avoid $request_filename. The vulnerable range starts at 0.1.29, so this code path is about as old as nginx itself.

CVE-2026-93479: regex captures in string expressions. A buffer overflow while "evaluating string expressions with variables, which affected the document root calculation and request logging in the http and stream modules". The trigger is a map that uses regex matching, where root or a log format references the regex's capture variables before the map's own output variable. F5's workaround is to use named captures instead of numbered ones, and to use them only inside the block where the regex matched. nginx fixed a different map-and-regex buffer overflow (CVE-2026-42533) in July's 1.30.4, so maps with regexes deserve a second look this year. (For May's rewrite-module overflow, see our post on CVE-2026-42945.)

CVE-2026-97205: AIO and SSI. With asynchronous file I/O on, an SSI page containing include virtual commands can trigger a use-after-free. The workaround is to disable aio or stop serving SSI pages.

CVE-2026-95101: HTTP/3 request body injection. With HTTP/3, "a part of an unknown frame might be treated as the request body". That can bypass client_max_body_size or inject data into the request nginx forwards upstream. It only matters where nginx proxies HTTP/3 traffic with request buffering on, which is the default. F5's workaround is proxy_request_buffering off.

CVE-2026-97629: uninitialised session ticket key. When nginx rotates TLS session ticket keys itself, which it does with ssl_session_cache shared:name:size and no ssl_session_ticket_key file, "one of the keys was left uninitialized". An attacker could forge a session, and F5 notes this could bypass client certificate verification. It matters most for servers using ssl_verify_client. The workarounds are to load static ticket keys from a file or remove the shared session cache. Turning off session tickets with ssl_session_tickets off should also take the ticket keys out of the picture, though F5 doesn't list it as a workaround.

The credits in the changelog go to Matt Suiche of Tolmo Inc. (CVE-2026-93479), Hansol Lee of GOTROOT (CVE-2026-95101), PhiNode Labs (CVE-2026-97629) and cyberlancer (CVE-2026-97205). F5's record credits F5 itself as the finder of CVE-2026-15176.

Check your configuration

nginx -T prints the full effective configuration, including every included file. This script parses that output and flags the five patterns. It's a heuristic: it reads the config, not traffic, so it will over-report in some edge cases and can miss configurations built from unusual includes or variables. Treat REVIEW as "look at this directive", not as proof of exposure. It uses only the standard library.

#!/usr/bin/env python3
"""Flag nginx configuration patterns tied to the five CVEs fixed in nginx 1.30.6 / 1.31.7
(14 October 2026). A heuristic: it finds configs worth reviewing, it doesn't prove exposure.
Usage: nginx -T 2>/dev/null | python3 nginx_oct2026_check.py   (or pass config files)"""
import re, sys

def tokens(text):
    for m in re.finditer(r'#[^\n]*|"(?:\\.|[^"])*"|\'(?:\\.|[^\'])*\'|[{};]|[^\s{};#]+', text):
        if not m.group(0).startswith("#"):
            yield m.group(0)

def parse(text):
    """Return a tree of (name, args, children) for every directive and block."""
    root, stack, words = [], [], []
    for t in tokens(text):
        if t in (";", "{"):
            node = (words[0], words[1:], [])
            (stack[-1][2] if stack else root).append(node)
            if t == "{":
                stack.append(node)
            words = []
        elif t == "}":
            stack.pop()
        else:
            words.append(t)
    return root

def walk(nodes, path=()):
    for n in nodes:
        yield n, path
        yield from walk(n[2], path + (n,))

def where(path, n):
    ctx = " > ".join(p[0] + (" " + " ".join(p[1]) if p[1] else "") for p in path if p[0] in ("http", "stream", "server", "location", "map"))
    return f"{ctx or 'top level'}: {n[0]} {' '.join(n[1])}"[:140]

text = "".join(open(f).read() for f in sys.argv[1:]) if sys.argv[1:] else sys.stdin.read()
tree, findings = parse(text), []
nodes = list(walk(tree))
uses = lambda n, var: any(var in a for a in n[1])

# CVE-2026-15176: alias + rewrite + set/return using $request_filename, or $request_filename in log_format
for n, path in nodes:
    if n[0] == "location":
        names = {c[0] for c in n[2]}
        rf = [c for c in n[2] if c[0] in ("set", "return") and uses(c, "$request_filename")]
        if "alias" in names and "rewrite" in names and rf:
            findings.append(("CVE-2026-15176", where(path + (n,), rf[0])))
    if n[0] == "log_format" and uses(n, "$request_filename"):
        findings.append(("CVE-2026-15176", where(path, n)))

# CVE-2026-93479: regex map whose numbered captures are used in root or log_format
regex_maps = any(n[0] == "map" and any(c[0].startswith("~") for c in n[2]) for n, _ in nodes)
for n, path in nodes:
    if regex_maps and n[0] in ("root", "log_format") and re.search(r"\$[1-9]\b", " ".join(n[1])):
        findings.append(("CVE-2026-93479", where(path, n)))

# CVE-2026-95101: HTTP/3 listener in a server that proxies with request buffering left on
for n, path in nodes:
    if n[0] == "server":
        inner = [c for c, _ in walk(n[2])]
        h3 = any((c[0] == "listen" and "quic" in c[1]) or (c[0] == "http3" and c[1] == ["on"]) for c in inner)
        proxies = any(c[0] == "proxy_pass" for c in inner)
        unbuffered = any(c[0] == "proxy_request_buffering" and c[1] == ["off"] for c in inner)
        if h3 and proxies and not unbuffered:
            findings.append(("CVE-2026-95101", where(path, n) + " (HTTP/3 + proxy_pass, request buffering on)"))

# CVE-2026-97629: shared session cache with automatic ticket keys (no ssl_session_ticket_key)
cache = [(n, p) for n, p in nodes if n[0] == "ssl_session_cache" and any(a.startswith("shared:") for a in n[1])]
static_keys = any(n[0] == "ssl_session_ticket_key" for n, _ in nodes)
tickets_off = any(n[0] == "ssl_session_tickets" and n[1] == ["off"] for n, _ in nodes)
client_certs = any(n[0] == "ssl_verify_client" and n[1] and n[1][0] != "off" for n, _ in nodes)
for n, p in cache:
    if not static_keys and not tickets_off:
        findings.append(("CVE-2026-97629", where(p, n) + (" (ssl_verify_client in use)" if client_certs else "")))

# CVE-2026-97205: aio together with SSI
aio = [(n, p) for n, p in nodes if n[0] == "aio" and n[1] and n[1][0] != "off"]
if aio and any(n[0] == "ssi" and n[1] == ["on"] for n, _ in nodes):
    findings.append(("CVE-2026-97205", where(*aio[0][::-1]) + " (with ssi on)"))

for cve, detail in findings:
    print(f"REVIEW  {cve}  {detail}")
print(f"{len(findings)} pattern(s) found. Fixed versions: nginx 1.30.6 (stable), 1.31.7 (mainline).")
sys.exit(1 if findings else 0)

We tested it on a deliberately risky config that contains every pattern:

http {
    log_format files '$remote_addr $request_filename $status';
    map $uri $legacy_root {
        ~^/docs/(\w+)/   /srv/docs;
        default          /srv/www;
    }
    ssl_session_cache shared:SSL:10m;
    aio threads;
    server {
        listen 443 ssl;
        listen 443 quic reuseport;
        ssl_verify_client optional;
        location /static/ {
            alias /srv/assets/;
            rewrite ^/static/old/(.*)$ /static/$1 last;
            set $target $request_filename;
        }
        location /docs/ {
            root /srv/docs/$1;
            ssi on;
        }
        location /api/ {
            proxy_pass http://backend;
        }
    }
}

Here are the results for that config and for a safer version (safe.conf) that uses named captures, a static ticket key, proxy_request_buffering off and no SSI:

$ python3 nginx_oct2026_check.py risky.conf
REVIEW  CVE-2026-15176  http: log_format files '$remote_addr $request_filename $status'
REVIEW  CVE-2026-15176  http > server > location /static/: set $target $request_filename
REVIEW  CVE-2026-93479  http > server > location /docs/: root /srv/docs/$1
REVIEW  CVE-2026-95101  http: server  (HTTP/3 + proxy_pass, request buffering on)
REVIEW  CVE-2026-97629  http: ssl_session_cache shared:SSL:10m (ssl_verify_client in use)
REVIEW  CVE-2026-97205  http: aio threads (with ssi on)
6 pattern(s) found. Fixed versions: nginx 1.30.6 (stable), 1.31.7 (mainline).
$ echo $?
1
$ python3 nginx_oct2026_check.py safe.conf
0 pattern(s) found. Fixed versions: nginx 1.30.6 (stable), 1.31.7 (mainline).
$ echo $?
0

On a real server, run nginx -T 2>/dev/null | python3 nginx_oct2026_check.py. It needs to run as a user who can read the configuration files, usually root.

Upgrading

Check what you're running with nginx -v. The fixed upstream versions are 1.30.6 and 1.31.7. NGINX Plus fixes are 37.1.0.1 and 37.0.5.1 according to F5's records, and F5 lists R34 to R36 as affected. If you use distribution packages, watch your distribution's tracker. When we checked on 14 October, Debian's security tracker and Ubuntu's OSV feed didn't list these CVEs yet. nginx's own packages at nginx.org are usually the fastest route to the fixed versions.

Remember the embedded copies too: container images with nginx baked in, ingress controllers, and appliances that bundle nginx. F5 also published separate CVEs for NGINX Ingress Controller and NGINX Gateway Fabric on the same day, covering different bugs in those products, so check their own advisories and fixed versions rather than assuming the nginx version number applies.

A busy year for nginx

nginx.org's advisory page lists 26 advisories with 2026 CVE IDs, compared with 2 with 2025 IDs and 7 with 2024 IDs. Every stable 1.30.x release since 1.30.1 in May has carried at least one security fix. Most of the bugs, like this month's, depend on specific configurations and are rated medium. That changes how you should patch nginx. A monthly check of nginx.org's advisories against your configuration, with a tool like the one above, tells you whether a release is urgent for you or can wait for your normal cycle.

The fair view

None of these five are exploitable in a default nginx configuration, and nginx.org rates them all medium. The memory-safety bugs need ASLR to be disabled or bypassed before they lead to code execution, and without that they crash a worker process, which nginx restarts. F5's 9.2 scores reflect the worst case.

But the patterns aren't exotic. alias with rewrite, $request_filename in logs, regex maps feeding root, and shared session caches all turn up in real configurations, often copied from old examples. If your config matches, upgrade this week. If it doesn't, upgrade on schedule and keep the check in your pipeline for next month's release.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.