-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The patch is one line. The line sits directly above a 2012 line.\n\nThe diff for CVE-2026-42945 is small enough to read in full:\n\n```diff\n@@ -1202,6 +1202,7 @@ ngx_http_script_regex_end_code(ngx_http_script_engine_t *e)\n r = e->request;\n\n+ e->is_args = 0;\n e->quote = 0;\n```\n\nThat is the entire fix. One line. Above it, untouched, sits a line that has been in nginx since release 0.1.29, dated 12 May 2005. The 2026 commit, authored by Roman Arutyunyan on 22 April 2026, is titled *Rewrite: fixed escaping and possible buffer overrun*. The commit body credits Zhenpeng \"Leo\" Lin and points to a prior fix:\n\n> A similar issue was fixed in 74d939974d43.\n\nCommit `74d939974d43` is from 11 May 2012. Maxim Dounin. Title: *Rewrite: fixed escaping and possible segfault*. The bug report it closed was ticket #162. The two commit messages open with the same clause: *If there were arguments in a rewrite's replacement string*. The same clause, fourteen years apart, against the same flag in the same function. The 2012 commit removed a line that copied `is_args` from a sub-engine. The 2026 commit zeros `is_args` at the end of the regex code. They are halves of one reset.\n\n## The engine has two flags and one predicate that reads both\n\nNGINX rewrites run in two passes through a compiled script engine. First a length pass computes how big the output buffer needs to be. Then a copy pass writes into a buffer of exactly that size. The engine carries state in `ngx_http_script_engine_t`. Two fields of that struct matter here:\n\n```c\nunsigned is_args:1;\nunsigned quote:1;\n```\n\n`is_args` means \"the engine has crossed the `?` and is now writing query-string bytes.\" `quote` means \"we are inside a quoted context.\" Each is set, used, and reset across the lifetime of a rewrite. Each is read by the same predicate. From `ngx_http_script_copy_capture_len_code` and `ngx_http_script_copy_capture_code`:\n\n```c\nif ((e->is_args || e->quote) && (r->quoted_uri || r->plus_in_uri)) {\n len += 2 * ngx_escape_uri(NULL, &r->captures_data[code->n],\n code->m, NGX_ESCAPE_ARGS);\n}\n```\n\nIf either flag is set, and the incoming request had quoted characters or `+` signs, the length pass adds `2 * ngx_escape_uri(...)` bytes to its estimate. Three output bytes per escapable input byte: a `%`, two hex digits. That `2 *` reflects the expansion. The copy pass uses the same predicate and either escapes or memcpy's accordingly.\n\nBoth flags can leak. Both flags can produce a length pass that disagrees with the copy pass. Both flags will, when leaked, produce a buffer with the wrong size and a write that runs past its end. The fix in 2012 patched the `is_args` leak path through the sub-engine. The fix in 2026 patches the `is_args` leak path through the main engine. The fix for `quote` in `regex_end_code`, the one written by Igor Sysoev twenty-one years ago, has held since the day it was written. The fact that it sat there alone for fourteen years is the bug.\n\n## How the 2026 leak path works\n\nLook at `ngx_http_script_start_args_code`. The compiled rewrite invokes it when the engine reaches a `?` in the replacement string. The whole body is one line:\n\n```c\ne->is_args = 1;\n```\n\nThat flag now points at every subsequent capture and copy operation. The replacement string keeps writing into the URI and arguments. Eventually the regex completes. Control passes to `ngx_http_script_regex_end_code`. Before the patch, that function did this:\n\n```c\ne->quote = 0;\n```\n\nIt did not touch `is_args`. The flag stayed set for as long as the request lived. The engine moved on to whatever script code came next, including operations that have nothing to do with query strings. From `ngx_http_script_complex_value_code`:\n\n```c\nngx_memzero(&le, sizeof(ngx_http_script_engine_t));\nle.ip = code->lengths->elts;\nle.line = e->line;\nle.request = r;\nle.quote = e->quote;\n```\n\nA new sub-engine is zeroed for the length pass of a complex value, then its `quote` field is loaded from the parent engine. `is_args` on the sub-engine is left at zero, because the memzero just ran. The length pass of `set $var ...` therefore computes a size that assumes `is_args=0`. No escape budget gets added.\n\nThen the copy pass runs on the parent engine. The parent's `is_args` is still 1. The capture-copy predicate fires. `ngx_escape_uri` runs with `NGX_ESCAPE_ARGS`. Every reserved byte in the captured group expands from one byte to three. The destination buffer was sized for one byte. The difference writes off the end.\n\nThe 2012 commit explains the same shape from the other direction:\n\n> if there were arguments in a rewrite's replacement string, and length was actually calculated... the is_args flag was set and incorrectly copied after length calculation. This resulted in escaping applied... buffer was allocated without escaping expected, thus this also resulted in buffer overrun and possible segfault.\n\nSame flag. Same function. Same buffer. Different path in.\n\n## What an exploit body looks like\n\nPublic PoCs for CVE-2026-42945 share a structure. From `jelasin/poc.py`, the trigger payload:\n\n```python\npayload = \"A\" * 349 + \"+\" * 969 + target_bytes.decode(\"latin-1\")\n```\n\n349 ASCII alphas, 969 `+` signs, 6 bytes of attacker-chosen address. The vulnerable server config is six lines:\n\n```nginx\nlocation ~ ^/api/(.*)$ {\n rewrite ^/api/(.*)$ /internal?migrated=true;\n set $original_endpoint $1;\n}\n```\n\nThe `rewrite` directive contains a `?`. The start-args opcode fires. `e->is_args` is now 1. The regex captures `(.*)` which is the attacker payload. Then `regex_end_code` runs and does not reset `is_args`. Then `set $original_endpoint $1` runs.\n\nThe 969 plus-signs each expand from one byte to three under `NGX_ESCAPE_ARGS`. 969 times 2 extra bytes is 1938 bytes of overrun. The target address is the cleanup-handler pointer in a sprayed `ngx_pool_cleanup_s`. From `make_body`:\n\n```python\nbuf = struct.pack('handler(c->data)`. That is `system(cmd)`.\n\nThe cleanup chain is interesting because it is the canonical post-heap-corruption pivot in this codebase. Anything that gets a single 8-byte write into a pool-attached structure can reach RCE through this primitive. The 2012 patch did not have to confront it directly, because the bug class was a segfault. The 2026 patch is rated the same way in the official advisory, \"worker process crash and possible code execution,\" but the public PoCs land in the second clause within seventy-two hours of disclosure.\n\n## The 2012 commit names the flag. The 2026 commit names it again.\n\nSide by side:\n\n> **2012, commit 74d939974d43, by Maxim Dounin:**\n> If there were arguments in a rewrite's replacement string, and length was actually calculated (i.e. there were complex values to substitute), the is_args flag was set and incorrectly copied after length calculation. This resulted in escaping applied to original args (if any) on the second iteration, while buffer was allocated without escaping expected, thus this also resulted in buffer overrun and possible segfault.\n\n> **2026, commit referenced as CVE-2026-42945, by Roman Arutyunyan:**\n> If there were arguments in a rewrite's replacement string, the is_args flag was set and incorrectly never cleared. This resulted in escaping applied... A similar issue was fixed in 74d939974d43. Reported by Leo Lin.\n\nThe two messages reuse the same first clause. Both name `is_args`. Both end at buffer overrun. The 2012 fix removed one line:\n\n```diff\n- e->is_args = le.is_args;\n```\n\nThe sub-engine was writing the flag back to the main engine after length calculation. Dounin deleted the write. The flag could no longer leak from sub-engine to main.\n\nThe 2026 fix adds one line. Above the existing `e->quote = 0;` reset, after fourteen years of pretending one flag did not need symmetric treatment, the engine now zeros both:\n\n```c\ne->is_args = 0;\ne->quote = 0;\n```\n\n## The eighteen-year-old line\n\nThe `e->quote = 0;` line that sat alone is itself a renamed survivor. In release 0.6.27, dated 12 March 2008, Igor Sysoev rewrote the script engine's argument handling. That release renamed `e->args` to `e->is_args` and added the `ngx_http_script_mark_args_code` op. Both `copy_capture_len_code` and `copy_capture_code` were rewritten to read both flags. The predicate\n\n```c\nif ((e->is_args || e->quote) && (r->quoted_uri || r->plus_in_uri))\n```\n\nwas introduced in that same release. From the day the predicate was born, the two flags it reads have been governed by different reset policies. `quote` was reset in `regex_end_code` before the rename. `is_args`, freshly created in 2008 and given the same role as `quote` in the predicate, was not.\n\nEighteen years and one month later, on 22 April 2026, the asymmetry was finally noticed. One commit. One line. Directly above the line that should have had a sibling all along.\n\n## The PoC field is wider than the disclosure\n\nWithin seventy-two hours of the advisory, at least sixteen public repositories ship a working PoC. The samples are not minor variations. `jelasin/poc.py` ships a fork-deterministic spray that targets Ubuntu 22.04 with ASLR off. `cipherspy/nginx_rift_htb.py` ships a portable variant with libc offsets for Ubuntu 20.04, 22.04, and 24.04, Debian 11 and 12, and Alpine. `rheodev/vuln_analysis.c` is a complete Chinese-language root-cause writeup with a diagrammed exploit chain. The PoCs converge on the same trigger and the same primitive because there is only one trigger that works and only one primitive worth pivoting through. That convergence is itself a measurement: the bug is small enough to fit on one screen, and once you have read the patch you know how to reach it.\n\nThe same researcher, Zhenpeng \"Leo\" Lin, is credited on at least three of the five NGINX CVEs published in the 1.31.0 cycle. The other two in the cycle are escape-related as well. The script engine, the request line parser, and the SMTP state machine all surface defects on a cadence. A vendor that ships five CVEs in one cycle in the components that handle untrusted strings is telling you, with full reproducibility, where the architectural debt lives.\n\n## This is a design-debt-driver fingerprint\n\nThe [design-debt-driver pattern](/patterns/design-debt-driver) describes a component whose internal structure produces the same class of bug on a predictable schedule. The script engine's shape is the driver here. Two parallel flags, governed by one predicate, with asymmetric reset hooks. The 2012 fix patched one leak path through the sub-engine. The 2026 fix patches the other leak path through `regex_end_code`. Both fixes are one line of code. Both close the same buffer overrun. Both name the same flag in their commit titles. Fourteen years separate them.\n\nThe driver is not malicious. It is geometric. When a predicate fans in over multiple flags, and those flags have different reset policies, the predicate's reliability is the minimum of the individual flag reliabilities. The script engine has been near that minimum twice. It will be near it again the next time the predicate gains a third input or the reset block fails to grow.\n\n## The defender's read\n\nIf you run NGINX with rewrite rules that contain a `?` in the replacement string, and the captured groups can include `+` or percent-encoded bytes from an untrusted source, you were exposed. The advisory's \"worker process crash\" framing is a lower bound. The \"possible code execution\" framing is what the public PoCs achieve.\n\nUpgrade to 1.31.0. The patch is one line and ports cleanly. If you cannot upgrade, audit your rewrites for replacement strings that contain `?`. The vulnerable shape is any rewrite whose target URI introduces query arguments while the captured group flows into a subsequent `set` directive. The minimal config is six lines and one of them is the trigger.\n\nFor everyone else: the takeaway is that NGINX's script engine has now produced the same buffer-overrun in the same flag in the same function twice. The first time, in 2012, the fix removed a write. The second time, in 2026, the fix adds a reset. The reset is now symmetric. Until the predicate gains a third flag.","closing_line":"Two parallel flags. One reset point. Half a fix in 2012. The other half in 2026.","hook_md":"On 22 April 2026, Roman Arutyunyan committed one line to `ngx_http_script.c`. The line was `e->is_args = 0;`. It went directly above an existing line, `e->quote = 0;`, that had been sitting in the same function since May 2005. The commit message reads, in part: \"A similar issue was fixed in 74d939974d43.\" That referenced commit is from May 2012. Its title is *Rewrite: fixed escaping and possible segfault*. The 2026 title is *Rewrite: fixed escaping and possible buffer overrun*. The flags reset on the same line of a two-line block. Half of the block was added in 2012. The other half waited fourteen years for a researcher named Leo Lin to notice.","post_id":286,"slug":"nginx-cve-2026-42945-the-other-half-of-the-2012-patch","title":"CVE-2026-42945: The Other Half of the 2012 Patch","type":"initial","unreadable_sentence":"The 2012 patch added `e->quote = 0;`. The 2026 patch adds `e->is_args = 0;` directly above it."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCagdk9gAKCRDeZjl4jgkQ JtqEAQCcUVUdnZKGrlrIEJSGhN6WaBmuZkXlrzjeOs/mu973AgD/S5BuLoku+j2n mw056iHegc/52T4kS8AUNuAo51zxIQ0= =9Xbv -----END PGP SIGNATURE-----