REPRO-2026-00228: Verified Repro With Script Download
REPRO-2026-00228: nginx charset module segfaults when charset map uses utf-8 as source charset, causing a NULL pointer dereference and DoS.
REPRO-2026-00228 is verified against nginx · generic. Affected versions: < 29c23ad846787e8baa1390b2edca479eb63ea8d7 (exact releases not specified). Fixed in 29c23ad846787e8baa1390b2edca479eb63ea8d7. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00228.
What Is REPRO-2026-00228?
REPRO-2026-00228 reproduces a high-severity denial-of-service bug in the nginx charset module, where a misconfigured charset_map directive using utf-8 as the source charset causes a NULL pointer dereference and worker process crash. Pruva reproduced it (reproduction REPRO-2026-00228).
REPRO-2026-00228 Severity
REPRO-2026-00228 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
Affected nginx Versions
nginx · generic versions < 29c23ad846787e8baa1390b2edca479eb63ea8d7 (exact releases not specified) are affected.
How to Reproduce REPRO-2026-00228
pruva-verify REPRO-2026-00228 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00228/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for REPRO-2026-00228
- reached the target end-to-end
- crash observed
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
HTTP GET request to nginx location configured with charset_map utf-8 as source charset
- charset_map utf-8 in first column creates 256-byte single-byte table; recode_from_utf8() casts it to u_char** and dereferences table[n>>8] as a pointer
- SIGSEGV
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for REPRO-2026-00228
A misconfigured charset_map directive with utf-8 in the first column (source charset) causes nginx to create wrong-format single-byte conversion tables. When a subsequent HTTP request triggers the charset filter's recode_from_utf8() path, the 256-byte single-byte table is cast to u_char ** and dereferenced as a pointer array (table[n >> 8]), reading garbage bytes as a memory address and crashing the worker process with SIGSEGV (signal 11). The upstream fix (commit 29c23ad846787e8baa1390b2edca479eb63ea8d7) adds a configuration-time validation that rejects charset_map with utf-8 in the first column, preventing the invalid configuration from ever being loaded.
- Package/Component affected: nginx
src/http/modules/ngx_http_charset_filter_module.c(thengx_http_charset_filter_module) - Affected versions: nginx versions prior to commit
29c23ad846787e8baa1390b2edca479eb63ea8d7(tested on nginx/1.31.3 at parent commit8f3465ac7f02b0ae86304e1be4ed319abb9d2edb) - Risk level: High — any attacker who can send an HTTP request to a server configured with the vulnerable
charset_mapdirective causes an immediate worker process crash (denial of service). The crash occurs on every request to the affected location. - Consequences: Repeated requests cause continuous worker respawns and crashes, degrading server availability. The crash is deterministic and triggered by a single HTTP GET request.
Impact Parity
- Disclosed/claimed maximum impact: Denial of Service (DoS) via NULL pointer dereference / segfault in nginx worker process when processing requests with the misconfigured
charset_map. - Reproduced impact from this run: DoS confirmed — nginx worker process crashes with SIGSEGV (signal 11, core dumped) on every HTTP request to the affected location. The worker is killed immediately when processing response body data containing non-ASCII bytes through the
recode_from_utf8()code path. - Parity:
full— the reproduced segfault/DoS matches the claimed impact exactly. - Not demonstrated: No code execution or privilege escalation was claimed or observed; the impact is purely a DoS crash.
Root Cause
The charset filter module supports two table formats:
- Single-byte tables (256 bytes): used when neither charset in a
charset_mapis UTF-8. Each byte maps directly:table[src_byte] = dst_byte. - UTF-8 multi-byte tables (256 ×
NGX_UTF_LEN= 1024 bytes forsrc2dst, and an array ofu_char *pointers fordst2src): used when the destination charset (second column) is UTF-8.
The bug occurs because ngx_http_charset_map_block() decides which table format to allocate based solely on whether value[2] (the destination/second column) is "utf-8". When utf-8 appears in value[1] (the source/first column) and the destination is a single-byte charset (e.g., windows-1251), the code takes the else branch and allocates 256-byte single-byte tables for both src2dst and dst2src.
During request processing, the charset filter's body filter calls ngx_http_charset_recode_from_utf8() when ctx->from_utf8 is true (i.e., the source charset is UTF-8). This function casts ctx->table (the 256-byte buffer) to u_char **table and dereferences table[n >> 8] as a pointer:
table = (u_char **) ctx->table; // 256-byte buffer cast to pointer array
...
n = ngx_utf8_decode(&src, len); // decode UTF-8 sequence to codepoint
if (n < 0x10000) {
p = table[n >> 8]; // reads 8 bytes at offset (n>>8)*8 as a pointer
if (p) {
c = p[n & 0xff]; // dereferences the garbage pointer → SIGSEGV
For example, with Cyrillic а (U+0430, encoded as 0xD0 0xB0), ngx_utf8_decode returns n = 0x0430, so n >> 8 = 4. table[4] reads bytes 32–39 of the 256-byte buffer (values 32,33,34,35,36,37,38,39), which on little-endian 64-bit forms the garbage pointer 0x0000002726252423. Since this is non-NULL, p[n & 0xff] dereferences 0x0000002726252453 — an unmapped address — causing SIGSEGV.
Fix commit: 29c23ad846787e8baa1390b2edca479eb63ea8d7 — "Charset: disabled charset_map with utf-8 in the first column". The fix adds a check in ngx_http_charset_map_block() that rejects the configuration at parse time:
if (ngx_strcasecmp(value[1].data, (u_char *) "utf-8") == 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"\"charset_map\" with \"utf-8\" charset "
"should be given in the second column");
return NGX_CONF_ERROR;
}
Reproduction Steps
- Script:
bundle/repro/reproduction_steps.sh - What the script does:
- Locates pre-built nginx binaries from the project cache (vulnerable build at commit
8f3465ac7and fixed build at commit29c23ad84), with a fallback to clone-and-build from source. - Creates an HTML file containing real UTF-8 multi-byte characters (Cyrillic
а,б,в— bytes0xD0 0xB0, etc.) to trigger the non-ASCII code path. - Vulnerable test (×2): Writes an nginx config with
charset_map utf-8 windows-1251 { }+charset windows-1251+source_charset utf-8, starts nginx as a real TCP listener, sends an HTTP GET request via curl, and checks the error log forexited on signal 11(SIGSEGV). - Fixed test (×2): Writes the same config and runs
nginx -tto verify the config is rejected with the patch's error message. - Config acceptance contrast: Verifies the vulnerable binary accepts the config (exit 0) while the fixed binary rejects it.
- Writes
bundle/repro/runtime_manifest.jsonwith proof artifacts.
- Locates pre-built nginx binaries from the project cache (vulnerable build at commit
- Expected evidence: Two vulnerable attempts showing
worker process N exited on signal 11 (core dumped)in the error log, and two fixed attempts showing"charset_map" with "utf-8" charset should be given in the second column.
Evidence
Log file locations
bundle/logs/vuln_error_1.log— Vulnerable attempt 1 error log (segfault)bundle/logs/vuln_error_2.log— Vulnerable attempt 2 error log (segfault)bundle/logs/vuln_conf_1.conf/vuln_conf_2.conf— Vulnerable nginx configsbundle/logs/fixed_test_1.log/fixed_test_2.log— Fixed version config rejectionbundle/logs/vuln_config_accept.log— Vulnerable config acceptancebundle/repro/runtime_manifest.json— Runtime evidence manifest
Key excerpts
Vulnerable worker segfault (attempt 1):
2026/07/04 18:20:50 [alert] 30827#0: worker process 30829 exited on signal 11 (core dumped)
Vulnerable worker segfault (attempt 2):
2026/07/04 18:20:57 [alert] 30847#0: worker process 30849 exited on signal 11 (core dumped)
Fixed version config rejection:
nginx: [emerg] "charset_map" with "utf-8" charset should be given in the second column
nginx: configuration file ... test is successful → test failed (exit 1)
Vulnerable version config acceptance:
nginx: the configuration file ... syntax is ok
nginx: configuration file ... test is successful (exit 0)
Environment
- nginx/1.31.3 built with
--without-http_rewrite_module --without-http_gzip_module --with-cc-opt='-g -O0' - Vulnerable commit:
8f3465ac7f02b0ae86304e1be4ed319abb9d2edb(parent of fix) - Fixed commit:
29c23ad846787e8baa1390b2edca479eb63ea8d7 - gcc 15.2.0, Linux x86_64
Recommendations / Next Steps
- Apply the upstream fix (commit
29c23ad846787e8baa1390b2edca479eb63ea8d7) to rejectcharset_mapwithutf-8in the first column at configuration parse time. - Audit existing configurations for any
charset_mapdirectives usingutf-8as the source charset and remove or correct them. - Add a regression test that verifies
nginx -tfails whencharset_map utf-8 <charset> { }is present. - Consider defensive coding in
recode_from_utf8()to validate table format before casting, as defense-in-depth against similar misconfigurations.
Additional Notes
- Idempotency: The script uses randomized port bases to avoid TCP TIME_WAIT conflicts between consecutive runs. Verified to pass twice consecutively with exit code 0.
- Ticket config note: The ticket's exact map entry
D0B0 E0(a 2-byte hex value) is rejected even in the vulnerable version because the single-byte parsing path (elsebranch inngx_http_charset_map()) requires values ≤ 255. The vulnerability is triggered with any valid single-byte map entry (e.g.,C0 E0) or even an emptycharset_mapblock (charset_map utf-8 windows-1251 { }), since the table format mismatch occurs regardless of the entries. - Two crash paths: The
charset_map utf-8 <non-utf8>misconfiguration affects two request-time code paths:recode_to_utf8(whencharset utf-8; source_charset <non-utf8>;): performs an out-of-bounds read attable[*src * NGX_UTF_LEN]on the 256-byte buffer, causing response corruption ("zero size buf" alert) and connection failure.recode_from_utf8(whencharset <non-utf8>; source_charset utf-8;): casts the 256-byte buffer tou_char **and dereferencestable[n >> 8]as a pointer, causing a reliable SIGSEGV.- The reproduction uses the
recode_from_utf8path for its deterministic crash behavior. Both paths are eliminated by the same fix.
REPRO-2026-00228 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
ls -la /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/ 2>/dev/null && echo "---REPO---" && ls -la /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/repo/ 2>/dev/null && echo "---MIRRORS---" && ls -la /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/repo-mirrors/ 2>/dev/null{"cwd":"/data/pruva/runs/9d8f1c44-d55a-40b6-a6d3-7bbab514913b","duration_ms":8,"exit_code":0,"stderr":"","stdout":"total 0\ndrwxr-xr-x 1 vscode vscode 86 Jul 4 16:55 .\ndrwxr-xr-x 1 vscode vscode 2758 Jul 4 16:49 ..\ndrwxr-xr-x 1 vscode vscode 22 Jul 4 16:55 .pruva\ndrwxr-xr-x 1 vscode vscode 58 Jul 4 16:55 nginx-fixed\ndrwxr-xr-x 1 vscode vscode 58 Jul 4 16:55 nginx-vuln\ndrwxr-xr-x 1 vscode vscode 258 Jul 4 16:55 repo\ndrwxr-xr-x 1 vscode vscode 0 Jul 4 16:49 repo-mirrors\n---REPO---\ntotal 56\ndrwxr-xr-x 1 vscode vscode 258 Jul 4 16:55 .\ndrwxr-xr-x 1 vscode vscode 86 Jul 4 16:55 ..\ndrwxr-xr-x 1 vscode vscode 122 Jul 4 16:55 .git\ndrwxr-xr-x 1 vscode vscode 146 Jul 4 16:50 .github\n-rw-r--r-- 1 vscode vscode 23 Jul 4 16:50 .gitignore\n-rw-r--r-- 1 vscode vscode 5215 Jul 4 16:50 CODE_OF_CONDUCT.md\n-rw-r--r-- 1 vscode vscode 4954 Jul 4 16:50 CONTRIBUTING.md\n-rw-r--r-- 1 vscode vscode 1319 Jul 4 16:50 LICENSE\n-rw-r--r-- 1 vscode vscode 670 Jul 4 16:55 Makefile\n-rw-r--r-- 1 vscode vscode 14571 Jul 4 16:50 README.md\n-rw-r--r-- 1 vscode vscode 4857 Jul 4 16:50 SECURITY.md\n-rw-r--r-- 1 vscode vscode 1720 Jul 4 16:50 SUPPORT.md\nd… [truncated]cd /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/repo && echo "=== GIT HEAD ===" && git rev-parse HEAD && echo "=== GIT LOG (last 5) ===" && git log --oneline -5 && echo "=== STATUS ===" && git status --short && echo "=== FIXED COMMIT EXISTS? ===" && git cat-file -t 29c23ad846787e8baa1390b2edca479eb63ea8d7 2>&1 || echo "COMMIT NOT FOUND IN REPO"=== GIT HEAD ===
29c23ad846787e8baa1390b2edca479eb63ea8d7
=== GIT LOG (last 5) ===
29c23ad84 Charset: disabled charset_map with utf-8 in the first column
8f3465ac7 GH: explicitly set permissions in workflows
072f6fdba Revert "HTTP/2: fixed overlapping memcpy in CONTINUATION frames"
42f8df65b Add missing bounds check in ngx_{http,stream}_compile_complex_value()
2d71bdcf8 HTTP/2: fixed overlapping memcpy in CONTINUATION framesecho "=== nginx-vuln ===" && ls -la /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-vuln/ && echo "=== nginx-fixed ===" && ls -la /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-fixed/ && echo "=== objs ===" && ls -la /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/repo/objs/ 2>/dev/null && echo "=== Makefile ===" && cat /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/repo/Makefile 2>/dev/null{"cwd":"/data/pruva/runs/9d8f1c44-d55a-40b6-a6d3-7bbab514913b","duration_ms":9,"exit_code":0,"stderr":"","stdout":"=== nginx-vuln ===\ntotal 4\ndrwxr-xr-x 1 vscode vscode 58 Jul 4 16:55 .\ndrwxr-xr-x 1 vscode vscode 86 Jul 4 16:55 ..\n-rw-r--r-- 1 vscode vscode 41 Jul 4 16:55 .build_commit\ndrwxr-xr-x 1 vscode vscode 414 Jul 4 16:51 conf\ndrwxr-xr-x 1 vscode vscode 36 Jul 4 16:51 html\ndrwxr-xr-x 1 vscode vscode 0 Jul 4 16:51 logs\ndrwxr-xr-x 1 vscode vscode 28 Jul 4 16:55 sbin\n=== nginx-fixed ===\ntotal 4\ndrwxr-xr-x 1 vscode vscode 58 Jul 4 16:55 .\ndrwxr-xr-x 1 vscode vscode 86 Jul 4 16:55 ..\n-rw-r--r-- 1 vscode vscode 41 Jul 4 16:55 .build_commit\ndrwxr-xr-x 1 vscode vscode 414 Jul 4 16:53 conf\ndrwxr-xr-x 1 vscode vscode 36 Jul 4 16:53 html\ndrwxr-xr-x 1 vscode vscode 0 Jul 4 16:53 logs\ndrwxr-xr-x 1 vscode vscode 28 Jul 4 16:55 sbin\n=== objs ===\ntotal 3584\ndrwxr-xr-x 1 vscode vscode 192 Jul 4 16:55 .\ndrwxr-xr-x 1 vscode vscode 258 Jul 4 16:55 ..\n-rw-r--r-- 1 vscode vscode 43450 Jul 4 16:55 Makefile\n-rw-r--r-- 1 vscode vscode 17430 Jul 4 16:55 autoconf.err\n-rwxr-xr-x 1 vscode vscode 3535936 Jul 4 16:55 nginx\n-rw-r--r-- 1 v… [truncated]echo "=== vuln build_commit ===" && cat /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-vuln/.build_commit && echo "" && echo "=== fixed build_commit ===" && cat /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-fixed/.build_commit && echo "" && echo "=== vuln sbin ===" && ls -la /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-vuln/sbin/ && echo "=== fixed sbin ===" && ls -la /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-fixed/sbin/ && echo "=== vuln nginx version ===" && /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-vuln/sbin/nginx -V 2>&1 && echo "=== fixed nginx version ===" && /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-fixed/sbin/nginx -V 2>&1{"cwd":"/data/pruva/runs/9d8f1c44-d55a-40b6-a6d3-7bbab514913b","duration_ms":15,"exit_code":0,"stderr":"","stdout":"=== vuln build_commit ===\n8f3465ac7f02b0ae86304e1be4ed319abb9d2edb\n\n=== fixed build_commit ===\n29c23ad846787e8baa1390b2edca479eb63ea8d7\n\n=== vuln sbin ===\ntotal 6912\ndrwxr-xr-x 1 vscode vscode 28 Jul 4 16:55 .\ndrwxr-xr-x 1 vscode vscode 58 Jul 4 16:55 ..\n-rwxr-xr-x 1 vscode vscode 3535920 Jul 4 16:55 nginx\n-rwxr-xr-x 1 vscode vscode 3535920 Jul 4 16:51 nginx.old\n=== fixed sbin ===\ntotal 6912\ndrwxr-xr-x 1 vscode vscode 28 Jul 4 16:55 .\ndrwxr-xr-x 1 vscode vscode 58 Jul 4 16:55 ..\n-rwxr-xr-x 1 vscode vscode 3535936 Jul 4 16:55 nginx\n-rwxr-xr-x 1 vscode vscode 3535936 Jul 4 16:53 nginx.old\n=== vuln nginx version ===\nnginx version: nginx/1.31.3\nbuilt by gcc 15.2.0 (Ubuntu 15.2.0-16ubuntu1) \nconfigure arguments: --prefix=/data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/nginx-vuln --without-http_rewrite_module --without-http_gzip_module --with-cc-opt='-g -O0'\n=== fixed nginx version ===\nnginx version: nginx/1.31.3\nbuilt by gcc 15.2.0 (Ubuntu 15.2.0-16ubuntu1) \nconfigure arguments: --prefix=/data/pruva/projec… [truncated]cd /data/pruva/project-cache/c4735c66-0745-4763-8cc2-ea1c568cef2a/repo && echo "=== SHOW FIX COMMIT ===" && git show 29c23ad846787e8baa1390b2edca479eb63ea8d7 --stat && echo "=== DIFF OF FIX ===" && git show 29c23ad846787e8baa1390b2edca479eb63ea8d7 --format="%H %s%n%n%b"{"cwd":"/data/pruva/runs/9d8f1c44-d55a-40b6-a6d3-7bbab514913b","duration_ms":7,"exit_code":0,"stderr":"","stdout":"=== SHOW FIX COMMIT ===\ncommit 29c23ad846787e8baa1390b2edca479eb63ea8d7\nAuthor: Sergey Kandaurov <pluknet@nginx.com>\nDate: Thu Jul 2 18:35:34 2026 +0400\n\n Charset: disabled charset_map with utf-8 in the first column\n \n This undocumented configuration was never expected to work and\n resulted in immediate segfaults.\n \n Reported by ret2ddme.\n\n src/http/modules/ngx_http_charset_filter_module.c | 7 +++++++\n 1 file changed, 7 insertions(+)\n=== DIFF OF FIX ===\n29c23ad846787e8baa1390b2edca479eb63ea8d7 Charset: disabled charset_map with utf-8 in the first column\n\nThis undocumented configuration was never expected to work and\nresulted in immediate segfaults.\n\nReported by ret2ddme.\n\n\ndiff --git a/src/http/modules/ngx_http_charset_filter_module.c b/src/http/modules/ngx_http_charset_filter_module.c\nindex e0115e1e4..e2bbb0cac 100644\n--- a/src/http/modules/ngx_http_charset_filter_module.c\n+++ b/src/http/modules/ngx_http_charset_filter_module.c\n@@ -1197,6 +1197,13 @@ ngx_http_charset_map_block(ngx_conf_t *cf, ngx_command_t *cmd, void *con… [truncated]Artifacts and Evidence for REPRO-2026-00228
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix REPRO-2026-00228
Upgrade nginx · generic to 29c23ad846787e8baa1390b2edca479eb63ea8d7 or later.
FAQ: REPRO-2026-00228
How does the nginx charset_map denial-of-service happen?
charset_map, so it never validated the configuration at load time. Any HTTP request to a location configured with the invalid charset_map triggers the flawed recode_from_utf8() code path, deterministically crashing the worker process on every request.Which nginx versions are affected, and where is this fixed?
29c23ad846787e8baa1390b2edca479eb63ea8d7 are affected (exact tagged releases not specified); this reproduction was tested on nginx/1.31.3 at parent commit 8f3465ac7f02b0ae86304e1be4ed319abb9d2edb. The fix commit adds configuration-time validation that rejects charset_map directives with utf-8 in the first column.How severe is this nginx charset_map issue?
charset_map directive causes an immediate worker crash, and repeated requests degrade availability through continuous worker respawns.How can I reproduce this nginx charset_map crash?
charset_map directive that uses utf-8 as the source charset. Sending a single HTTP GET request to the affected location deterministically crashes the worker process with SIGSEGV; rebuilding at the fixed commit rejects the invalid configuration at startup instead.References for REPRO-2026-00228
Authoritative sources for REPRO-2026-00228 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.