nginx is not the bug. Two lines of your config are. CVE-2026-42945 on a live stand A critical nginx vulnerability, CVE-2026-42945, with a CVSS score of 9.2, was discovered by an AI agent in six hours, affecting nearly all nginx versions since 2008. The bug, a heap overflow in the URL rewriting module, was fixed in nginx 1.30.1 and 1.31.0. An engineer's test stand found that while 5.7 million servers may be exposed, actual vulnerable configurations are rare, with only one out of 35,633 configs found vulnerable. A critical nginx vulnerability, 9.2 on CVSS, sat in the code for eighteen years. It was found not by a human but by an AI agent, and six hours were enough. The news comes with a number attached: 5.7 million servers on the internet. Then two independent researchers run scanners over real nginx configurations from GitHub. The first looks at 1465 configs from 528 popular repositories and finds not a single vulnerable one in production. The second looks at 35633 configs and finds one, in an abandoned project from 2011. Between "5.7 million" and "one out of thirty five thousand" the gap is tens of thousands of times. I built a test stand to work out which of them is right, and to check whether my own server is lying there open. Short answer: both are right, because they count different things. Long answer below, with commands, logs, and a script that checks your config in a second. On 13 May 2026 nginx 1.30.1 and 1.31.0 shipped, closing CVE-2026-42945. A heap hole, in the URL rewriting module. The first version with it is 0.6.27, the last is 1.30.0, so almost the entire history of the product is caught in it. For commercial NGINX Plus, per NVD, it is releases R32 through R36, fixed in R37. The vulnerability was found by depthfirst, who ran their own AI agent for low-level code audit over the nginx sources. In six hours the agent found five memory problems, four of which nginx confirmed. This one is the most serious. One small detail worth flagging right away, because it pays off at the end: in the fix commit itself, the "reported by" field carries the name of a live human being, Leo Lin. The agent found it, but an engineer made it into the commit history. One more thing, since we are on the accuracy of numbers. Most sources put the age of the bug at eighteen years, and that adds up: version 0.6.27 came out in 2008. But at least one major outlet wrote "sixteen years", and that number spread further through retellings. The correct one is eighteen. The scores diverged immediately. NVD and F5 give 9.2 on CVSS v4.0 and 8.1 on v3.1. nginx itself, in its own security advisories list, marks it as medium. I will unpack the reason for that gap at the end, once it is visible what it grows out of. The fix is one line. Here it is in full, file src/http/ngx http script.c, function ngx http script regex end code: @@ -1202,6 +1202,7 @@ ngx http script regex end code ngx http script engine t e r = e- request; + e- is args = 0; e- quote = 0; To see why one line is worth 9.2, you need to look at how nginx substitutes regular expression captures. Substitution runs in two passes. First the engine counts how many bytes the result will take and allocates a buffer. Then the second pass copies the data in. As long as both passes count the same, everything is fine. Copying an unnamed capture, that is $1 through $9, works like this: if the data goes into the query string rather than the path, it has to be escaped. A space becomes %20, a plus becomes %2B, so one byte becomes three. The decision is made by the is args flag inside the engine. A rewrite directive with a question mark in the replacement string arms that flag: everything after the question mark is arguments now. Reasonable. There is a caveat here about the case where nothing follows the question mark, but I will come back to it in the section on the boundary. Then rewrite processing ends, and this is where the flag should have been cleared, and it was not. It stayed armed until the end of location processing. Now look at the set directive. It computes the length of the value through a separate, freshly zeroed engine where is args is zero. So it counts the length with no room for escaping. But the data is copied by the main engine, the one where the flag stayed armed from the previous rewrite. Copied with escaping. And in that zeroed engine one flag of the pair does get carried over from the main one, this line is right there in the code: le.quote = e- quote; . is args was forgotten next to it. Half the state carried over, half not, and each half on its own looks completely correct. Result: the buffer is allocated for the raw string, and an escaped string is written into it, which can be three times longer. Everything past the end goes outside the allocated memory. That is also why the descriptions of the vulnerability talk about unnamed captures. Ordinary variables like $myvar are copied by different code, which has no escaping check at all, they are simply carried across as is. Although, further down on the stand it will turn out that this rule is stated imprecisely, and the wording is a dangerous one, but that is in the section on the boundary. There is one more detail in the commit text that I like better than the bug itself. The author of the fix writes: "A similar issue was fixed in 74d939974d43". That is a commit from 2012, trac ticket number 162, the same class of error: the counting pass and the copying pass out of sync on the same flag. What is amusing is that back then it was fixed the other way around. In 2012 they removed the line that carried is args into the local engine, that is, they stopped passing the flag. In 2026 they added the line that clears it. Fourteen years between two halves of the same mistake. I build it on a VPS, everything stays on the loopback: a deliberately vulnerable nginx inside, and no reason for it to face outward. I take the config not from my own setup but verbatim from the text of the commit that fixed this. That is more honest, and nobody gets to ask whether I tuned the configuration to fit the result. worker processes 1; error log /var/log/nginx/error.log info; events { worker connections 1024; } http { access log off; server { listen 80; location / { rewrite ^ . /new?c=1; set $myvar $1; return 200 $myvar; } } } Five containers: the vulnerable nginx 1.30.0 with this config, three controls on the same version, and the patched 1.30.1 with the exact same config. One worker, so a crash is unambiguous. services: vuln: image: nginx:1.30.0-alpine container name: rift-vuln ports: - "127.0.0.1:8099:80" volumes: - ./conf/vuln.conf:/etc/nginx/nginx.conf:ro The other four services differ only in image, port and mounted config, so I show one. The request I hit it with: a long run of pluses in the path. curl "http://127.0.0.1:8099/++++++++++++++++++++++ ... ++++" The plus works on two fronts here. It arms the internal marker that there is something in the URI to escape, and it is itself subject to escaping, turning into three bytes instead of one. So it delivers the length growth needed. Result of the very first run: CONTAINER PORT HTTP CRASHES rift-vuln 8099 000 1 rift-ctl-noq 8098 200 0 rift-ctl-noconsumer 8097 200 0 rift-ctl-named 8096 200 0 rift-fixed 8095 200 0 The vulnerable stand did not answer at all, the connection was dropped. In the log: 2026/08/12 23:29:03 notice 1 1: signal 17 SIGCHLD received from 30 2026/08/12 23:29:03 alert 1 1: worker process 30 exited on signal 11 2026/08/12 23:29:03 notice 1 1: start worker process 31 Signal 11 is a segfault. The master immediately brings up a new worker, and the server keeps running. Remember that log line, we come back to it in the detection section. Here is where the interesting part starts, the reason the stand was built at all. I check what exactly in the configuration is responsible for the crash. I remove exactly one element at a time. Removed the question mark from the replacement string, kept everything else: rewrite ^ . /new; . No crash. There is nothing to arm the flag. Kept the question mark, removed the consumer : rewrite stays, the line set $myvar $1; is gone, replaced with return 200 "ok"; . No crash. The flag is armed, but there is nobody to inherit it. That second control matters more than it looks. A day after this fix another one shipped, for a different bug in the same module, with overlapping captures. And the shape of its attacking request is exactly the same, a long run of pluses. If my stand had crashed without a consumer too, I would have been dissecting the wrong vulnerability and the whole mechanics above would be wrong. It does not crash, so this really is the flag leaking. Replaced the unnamed capture with a named one : rewrite ^ ?