{"repro_id":"REPRO-2026-00171","version":7,"title":"nginx WebDAV: heap-buffer-overflow in COPY/MOVE with alias directive","repro_type":"security","status":"published","severity":"high","description":"`ngx_http_dav_module` handles WebDAV PUT/DELETE/MKCOL/COPY/MOVE. For COPY and MOVE it constructs the destination filesystem path from the request's `Destination:` header. When the matched `location` uses `alias /some/dir/;` (rather than `root`) and a prefix that doesn't match the URI segment 1:1, the length calculation that sizes the destination string buffer underestimates by the alias substitution offset. The destination filename is then written past the end of a heap allocation in the nginx worker process — a heap-buffer-overflow write reachable **unauthenticated** on any nginx host that turns on `dav_methods` under such a location.\n\nThe reachable side effects depend on what the overflow corrupts:\n- denial-of-service via worker crash and respawn loop;\n- in some configurations, arbitrary file write at controllable paths (the F5 advisory rates impact \"I:L A:H\");\n- possible escalation to RCE via worker-heap corruption (not guaranteed, depends on allocator state).","root_cause":"# RCA Report: CVE-2026-27654 — nginx WebDAV COPY/MOVE heap-buffer-overflow with alias directive\n\n## Summary\n\nCVE-2026-27654 is a heap-based buffer overflow in nginx's `ngx_http_dav_module` that triggers when processing WebDAV COPY or MOVE requests under a location that uses the `alias` directive. The vulnerability stems from an integer underflow in `ngx_http_map_uri_to_path()`: when the destination URI (`duri`) is shorter than the location prefix length (`clcf->alias`), the buffer size calculation `clcf->root.len + r->uri.len - alias + 1` underflows as an unsigned `size_t`, resulting in a near-zero or wrapped allocation. The subsequent `memcpy` of the alias root path and destination URI then overflows the tiny heap buffer. This is reachable unauthenticated by any client that can send a WebDAV COPY/MOVE request to an nginx instance with `dav_methods` enabled under an aliased location.\n\n## Impact\n\n- **Package/component affected**: nginx Open Source and Plus, `ngx_http_dav_module`\n- **Affected versions**: `0.5.13–0.9.7`, `1.0.0–1.28.2`, `1.29.0–1.29.6`\n- **Fixed versions**: `1.28.3` (stable), `1.29.7` (mainline)\n- **Risk level**: High (CVSS 3.1: 8.2, CVSS 4.0: 8.8)\n- **Consequences**: Worker process crash (DoS), potential arbitrary file write outside the intended directory, and possible escalation to RCE depending on heap allocator state.\n\n## Root Cause\n\nThe bug is located in `ngx_http_dav_copy_move_handler()` (`src/http/modules/ngx_http_dav_module.c`). Before computing the destination filesystem path, the handler temporarily overwrites `r->uri` with the parsed `Destination` header URI (`duri`) and calls `ngx_http_map_uri_to_path()`. That function computes the required buffer length with:\n\n```c\npath->len = clcf->root.len + reserved + r->uri.len - alias + 1;\n```\n\nAll operands are unsigned `size_t`. When `alias` (which stores the location prefix length) is larger than `clcf->root.len + r->uri.len + 1`, the subtraction wraps around. For example, with `location /davvvv/` (alias = 7) and `alias /a/` (root.len = 3), a destination URI of `/xx` (len = 2) produces:\n\n`3 + 2 - 7 + 1` → `5 - 7` wraps to `SIZE_MAX - 1`, then `+ 1` wraps back to `0`.\n\n`ngx_pnalloc(pool, 0)` returns a tiny pointer from the nginx pool. The following `ngx_copy(path->data, clcf->root.data, 3)` writes 3 bytes into a zero-byte allocation, and the subsequent `ngx_copy(last, r->uri.data + alias, r->uri.len - alias)` passes a negative-size parameter (`-5`) to `memcpy`, causing a massive out-of-bounds read/write.\n\n**Fix commit**: `9739e75` in the nginx repository (`Dav: destination length validation for COPY and MOVE.`). The patch adds an explicit check before the path mapping:\n\n```c\nif (clcf->alias && clcf->alias != NGX_MAX_SIZE_T_VALUE && duri.len < clcf->alias) {\n    return NGX_HTTP_BAD_REQUEST;\n}\n```\n\nThis rejects any COPY/MOVE request whose destination URI is shorter than the location prefix, preventing the underflow.\n\n## Reproduction Steps\n\nThe reproduction is fully automated in `repro/reproduction_steps.sh`.\n\nWhat the script does:\n1. Clones the nginx repository (or reuses the existing clone).\n2. Builds the vulnerable version (`release-1.29.6`) and the fixed version (`release-1.29.7`), both compiled with AddressSanitizer (`-fsanitize=address`).\n3. Creates a minimal nginx configuration with a location `/davvvv/` that maps via `alias` to a short directory path, and enables `dav_methods COPY MOVE`.\n4. Starts the **vulnerable** nginx binary in the foreground (`daemon off; master_process off;`), sends a `COPY` request with a deliberately short `Destination: http://127.0.0.1:8080/xx` header, and captures ASAN output via `ASAN_OPTIONS=log_path=...`.\n5. Repeats the same request against the **fixed** nginx binary and captures the HTTP response.\n6. Verifies that the vulnerable build crashes with an ASAN error while the fixed build returns HTTP `400 Bad Request` with no ASAN error.\n\n**Expected evidence**:\n- `logs/vulnerable_asan.txt` — ASAN report showing `negative-size-param: (size=-5)` inside `ngx_http_map_uri_to_path` called from `ngx_http_dav_copy_move_handler`.\n- `logs/fixed_curl.txt` — HTTP `400 Bad Request` response from nginx/1.29.7.\n\n## Evidence\n\n### Vulnerable build (release-1.29.6)\n\nASAN log (`logs/vulnerable_asan.txt`):\n\n```\n==16927==ERROR: AddressSanitizer: negative-size-param: (size=-5)\n    #0 ... in memcpy\n    #1 ... in ngx_http_map_uri_to_path src/http/ngx_http_core_module.c:1987\n    #2 ... in ngx_http_dav_copy_move_handler src/http/modules/ngx_http_dav_module.c:703\n    #3 ... in ngx_http_dav_handler src/http/modules/ngx_http_dav_module.c:194\n    #4 ... in ngx_http_core_content_phase src/http/ngx_http_core_module.c:1282\n    ...\nSUMMARY: AddressSanitizer: negative-size-param ... in memcpy\n```\n\nThe stack trace confirms the crash occurs inside `ngx_http_map_uri_to_path` while handling the COPY request in the DAV module.\n\n### Fixed build (release-1.29.7)\n\nHTTP response (`logs/fixed_curl.txt`):\n\n```\n<html>\n<head><title>400 Bad Request</title></head>\n<body>\n<center><h1>400 Bad Request</h1></center>\n<hr><center>nginx/1.29.7</center>\n</body>\n</html>\n\nHTTP_CODE:400\n```\n\nThe fixed binary rejects the malicious request with a `400 Bad Request` and logs:\n\n```\nclient sent invalid \"Destination\" header: \"http://127.0.0.1:8080/xx\"\n```\n\n### Environment\n\n- OS: Linux 6.18.5 x86_64\n- Compiler: GCC 13.3.0\n- ASAN flags: `-fsanitize=address -g -O1 -fno-omit-frame-pointer`\n- nginx configure: `--with-http_dav_module`\n\n## Recommendations / Next Steps\n\n1. **Upgrade immediately** to nginx `1.28.3` (stable) or `1.29.7` (mainline) or later.\n2. **Mitigation** (if upgrading is not immediately possible): disable WebDAV COPY and MOVE methods, or avoid using the `alias` directive in locations that expose DAV methods.\n3. **Regression testing**: any future changes to `ngx_http_dav_copy_move_handler` or `ngx_http_map_uri_to_path` should include a test case with a destination URI shorter than the location prefix under an aliased location.\n4. **Code review**: audit other callers of `ngx_http_map_uri_to_path` that temporarily modify `r->uri` to ensure similar length validations are in place.\n\n## Additional Notes\n\n- **Idempotency**: `repro/reproduction_steps.sh` has been executed twice consecutively on a clean environment and produced the same confirmed verdict both times.\n- **Edge cases**: The trigger requires a specific relationship between the alias path length, the location prefix length, and the destination URI length. The script uses `location /davvvv/` (len 7) + `alias /a/` (len 3) + destination `/xx` (len 2) to force the underflow to wrap to exactly `0`, which maximizes the chance of a detectable heap overflow. Other combinations (e.g., longer destination URIs that still underflow to small non-zero values) are also exploitable.\n- **Limitations**: The reproduction requires building nginx from source with ASAN, which takes several minutes. The script caches the built binaries in `$ROOT/builds/` to avoid repeated compilation.\n","cve_id":"CVE-2026-27654","cwe_id":"CWE-122 (Heap-based Buffer Overflow)","package":{"name":"nginx","ecosystem":"source","affected_versions":"OSS 0.5.13–0.9.7, 1.0.0–1.28.2, 1.29.0–1.29.6; Plus R32–R36"},"reproduced_at":"2026-05-28T12:07:11.635221+00:00","duration_secs":1300.820897102356,"tool_calls":173,"turns":151,"handoffs":3,"total_cost_usd":1.6797646399999997,"agent_costs":{"repro":0.5781945400000001,"support":0.01929566,"vuln_variant":1.08227444},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":0.5781945400000001},"support":{"accounts/fireworks/models/kimi-k2p6":0.01929566},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":1.08227444}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-28T12:07:14.895612+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":7003,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":4887,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":8209,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":7112,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":2544,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":655,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":5114,"category":"ticket"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1180,"category":"other"},{"path":"bundle/repro/nginx.conf","filename":"nginx.conf","size":476,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1378,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":4701,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3214,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":2583,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":1422,"category":"other"},{"path":"bundle/logs/vuln_prefix_curl_err.txt","filename":"vuln_prefix_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_exact_stdout.txt","filename":"vuln_exact_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_prefix_curl_err.txt","filename":"fixed_prefix_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_stderr.txt","filename":"fixed_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_urlenc_curl.txt","filename":"vuln_urlenc_curl.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_nested_curl_err.txt","filename":"vuln_nested_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/test_stderr.txt","filename":"test_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_prefix_stderr.txt","filename":"fixed_prefix_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_nested_curl_err.txt","filename":"fixed_nested_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/vulnerable_asan.txt","filename":"vulnerable_asan.txt","size":3262,"category":"other"},{"path":"bundle/logs/fixed_script_curl.txt","filename":"fixed_script_curl.txt","size":172,"category":"other"},{"path":"bundle/logs/vuln_prefix.18268","filename":"vuln_prefix.18268","size":3262,"category":"other"},{"path":"bundle/logs/vuln_move_stderr.txt","filename":"vuln_move_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_exact_curl.txt","filename":"fixed_exact_curl.txt","size":172,"category":"other"},{"path":"bundle/logs/fixed_nested_stdout.txt","filename":"fixed_nested_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/asan_vulnerable.17016","filename":"asan_vulnerable.17016","size":3262,"category":"other"},{"path":"bundle/logs/variant_final.log","filename":"variant_final.log","size":1116,"category":"log"},{"path":"bundle/logs/vuln_exact_curl_err.txt","filename":"vuln_exact_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_curl_err.txt","filename":"fixed_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_urlenc_stderr.txt","filename":"fixed_urlenc_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_script_curl_err.txt","filename":"fixed_script_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_script_stdout.txt","filename":"vuln_script_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_nested_stderr.txt","filename":"fixed_nested_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_move_stdout.txt","filename":"fixed_move_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_script_stdout.txt","filename":"fixed_script_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_move_stdout.txt","filename":"vuln_move_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_move_curl_err.txt","filename":"fixed_move_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/error.log","filename":"error.log","size":8781,"category":"log"},{"path":"bundle/logs/vuln_script_curl.txt","filename":"vuln_script_curl.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_script_stderr.txt","filename":"fixed_script_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_prefix_stdout.txt","filename":"vuln_prefix_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_move_stderr.txt","filename":"fixed_move_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_move.18292","filename":"vuln_move.18292","size":3262,"category":"other"},{"path":"bundle/logs/fixed_nested_curl.txt","filename":"fixed_nested_curl.txt","size":172,"category":"other"},{"path":"bundle/logs/vuln_script.18286","filename":"vuln_script.18286","size":3262,"category":"other"},{"path":"bundle/logs/vuln_script_curl_err.txt","filename":"vuln_script_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_nested_stdout.txt","filename":"vuln_nested_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/vulnerable_curl_err.txt","filename":"vulnerable_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_exact_stdout.txt","filename":"fixed_exact_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_nested.18280","filename":"vuln_nested.18280","size":3262,"category":"other"},{"path":"bundle/logs/test_stdout.txt","filename":"test_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_prefix_stdout.txt","filename":"fixed_prefix_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_urlenc_curl_err.txt","filename":"fixed_urlenc_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_nested_stderr.txt","filename":"vuln_nested_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/access.log","filename":"access.log","size":3380,"category":"log"},{"path":"bundle/logs/vuln_script_stderr.txt","filename":"vuln_script_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/vulnerable_stdout.txt","filename":"vulnerable_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_urlenc_stdout.txt","filename":"vuln_urlenc_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/variant_tests.log","filename":"variant_tests.log","size":76,"category":"log"},{"path":"bundle/logs/vuln_prefix_curl.txt","filename":"vuln_prefix_curl.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_nested_curl.txt","filename":"vuln_nested_curl.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_move_curl.txt","filename":"fixed_move_curl.txt","size":172,"category":"other"},{"path":"bundle/logs/vuln_move_curl_err.txt","filename":"vuln_move_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/vulnerable_curl.txt","filename":"vulnerable_curl.txt","size":0,"category":"other"},{"path":"bundle/logs/test_curl_err.txt","filename":"test_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/test_curl.txt","filename":"test_curl.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_urlenc_stderr.txt","filename":"vuln_urlenc_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_move_curl.txt","filename":"vuln_move_curl.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_exact_stderr.txt","filename":"fixed_exact_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/nginx.pid","filename":"nginx.pid","size":6,"category":"other"},{"path":"bundle/logs/vuln_exact.18274","filename":"vuln_exact.18274","size":3262,"category":"other"},{"path":"bundle/logs/vuln_urlenc_curl_err.txt","filename":"vuln_urlenc_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_urlenc_stdout.txt","filename":"fixed_urlenc_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_curl.txt","filename":"fixed_curl.txt","size":172,"category":"other"},{"path":"bundle/logs/vulnerable_stderr.txt","filename":"vulnerable_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_stdout.txt","filename":"fixed_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_exact_curl.txt","filename":"vuln_exact_curl.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_prefix_stderr.txt","filename":"vuln_prefix_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_exact_stderr.txt","filename":"vuln_exact_stderr.txt","size":0,"category":"other"},{"path":"bundle/logs/vuln_urlenc.18298","filename":"vuln_urlenc.18298","size":3305,"category":"other"},{"path":"bundle/logs/fixed_exact_curl_err.txt","filename":"fixed_exact_curl_err.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_urlenc_curl.txt","filename":"fixed_urlenc_curl.txt","size":172,"category":"other"},{"path":"bundle/logs/fixed_prefix_curl.txt","filename":"fixed_prefix_curl.txt","size":172,"category":"other"}]}