Claude + Humans vs nginx: CVE-2026-27654
What humans still do when the Claude already found the bug.
We'd like to acknowledge Claude, Anthropic Research, NGINX developers and F5 PSIRT for partnering with us on this. It was a pleasant experience.
By now we know AI can find real vulnerabilities and write working exploits. That part is no longer surprising.
The more interesting question is the human role. Where does human expertise still matter when the initial bug report is already correct? What separates a crash from a real exploit? What does collaboration look like in practice, on a real vulnerability with a real fix and a real disclosure?
CVE-2026-27654 is a useful case. The bug needs a non-default config: ngx_http_dav_module compiled in, and a location combining alias with dav_methods COPY or MOVE. The exposed population is small. Inside that population the bug is severe.
Claude flagged it correctly: a heap buffer overflow in ngx_http_dav_copy_move_handler(), driven by an unsigned underflow in ngx_http_map_uri_to_path() when the Destination header is shorter than the location prefix. Claude provided a working crash:
COPY /dav/x HTTP/1.1
Host: localhost
Destination: /da <-- shorter than "/dav/" -> underflowThat crashes a worker. Whether it can do more than that is a harder question, and at least for now, answering it takes a human.
What it does, when it works: it escapes the WebDAV root. The alias directive is supposed to be a jail; a COPY against location /dav/ { alias /var/dav/uploads/; } should only ever touch files under /var/dav/uploads/. The bug lets a remote attacker read or write files anywhere the worker UID can reach.
Three of us worked through this with Claude independently, each in our own session, comparing notes between rounds. The independence mattered: the same prompt to two different Claude conversations produced one "impossible" and one working exploit (more on that under Round two). The first exploit out of the gate was a clean repro we could ship to F5; the refinements that followed came from looking at what each of us had built and asking which precondition felt least likely to exist on a real target.
Round one: aim high (PoC-1). Arbitrary file write with attacker-chosen content. PUT a webshell under the WebDAV root, then trigger the overflow on COPY to copy it to /var/www/html/x.php. Claude built it; it worked. But the heap groom needs the source-path buffer pushed into a separate malloc() block, which means a request URI over 4000 characters, which means the PUT must land in a directory tree twenty levels deep with ~200-character folder names. nginx builds that tree if you set create_full_put_path on, but "the server accepts arbitrarily long PUT paths" is not a precondition you find often.
Round two: give up on write (PoC-2). The question we put to Claude:
We don't actually need to write our own bytes. If we control both the source and the destination of the COPY, can we copy a file that already exists, like
/etc/passwd, into a download folder we can fetch it from?
Two of us asked independently. One Claude said it was impossible. The other produced a working exploit first try: a single COPY, short URI so the source path stays in the request pool adjacent to the destination, and the same overflow rewrites both paths at once. That became PoC-2.
The first thing we tested after it worked was whether it was as clean as it looked. The draft of this writeup said the worker "never crashes."
This is not true, right? Because the second PoC did crash workers if memcpy didn't hit that lucky condition.
It hadn't checked. We made it sweep all 16 alignment residues; two of them crash before any file is touched. The "never" became "on 14 of 16 alignments."
Then the constraint. The traversal injected into the source path is 20 characters, fixed by the header structure. Claude's first count of how those 20 split was wrong:
With a 3-level surviving prefix you spend 12 characters on
/../../../and have 8 left for the filename. Is this a correct assessment?
It wasn't. /../../../ is 10, not 12; etc/passwd is 10, not 8. (Note to self: never ask Claude to file our tax returns.) Ten and ten, and etc/passwd fits exactly. We asked whether the constraint itself could be stretched and the answer was: not by changing the URI length (both endpoints of the controlled window shift together), but yes by tuning the header-key lengths, which we ended up doing in §6.3.
Round three happened while we were writing this document (slash-padding variant). We were fact-checking why the deep PUT tree in PoC-1 is unavoidable, and the chain went like this:
Can you do something like this to artificially expand the length?
COPY /etc/../etc/../etc/../etc/../passwd HTTP/1.1
No. nginx normalizes .. before r->uri.len is set; the padding gets stripped.
Does it also normalize the source path in
COPY <source_path>? We want a long source-path string to push it into its own malloc, but at the same time we want it to resolve to a short path on the filesystem. Is that possible?
That was the question that mattered. Claude tested /., //, %2e%2e: all collapsed. Then it tried merge_slashes off. With that one directive, nginx stops collapsing // but the kernel still does (POSIX path resolution). So /dav/ + 4000 slashes + p.php is a 4010-character URI to nginx and the same inode as /dav/p.php to lstat(). Worked first try. The deep tree, create_full_put_path, the long folder names: all gone, traded for one line of config that exists in the wild for unrelated reasons.
So three variants, each one found by asking what's actually load-bearing in the previous one's preconditions. The most ambitious primitive came first and was the most expensive; the simplest deployment story came last and only because we were poking at why the expensive one was expensive.
A pattern we noticed: left to itself, Claude reached for the most powerful primitive and accepted whatever preconditions came with it. The first exploit was file write, the strongest thing the bug could give, and it worked, and it would also almost never apply to a real server. The two moves that made the bug practically dangerous were both human: stepping down to a weaker primitive (file read) to shed preconditions, and then much later, asking whether one of the original preconditions was even real. Claude could test those ideas faster than we could, but it didn't generate them. Maybe that's just because nobody told it that "works in a Docker container we built" is not the same as "works on a server someone else runs"; maybe that judgment is harder to teach than the heap layout. Either way, the division of labour was consistent: we picked which constraint to attack, it did the byte-level work to attack it.
The issue was disclosed to F5, which fixed it and published an advisory acknowledging:
Calif.io in collaboration with Claude and Anthropic Research for bringing this issue to our attention and following the highest standards of coordinated disclosure.
Timeline:
2026-02-XX: Vulnerability discovered
2026-03-10: Reported to F5 / nginx security team
2026-03-11: F5 acknowledged the report
2026-03-24: nginx 1.29.7 released with fix; F5 advisory K000160382 published; CVE-2026-27654 assigned
2026-03-24: Fix commit independently noticed at spaceraccoon/vulnerability-spoiler-alert#102
2026-04-10: This writeup published
Two of those rows are the same date. The fix landed in public on the 24th; an AI-powered commit watcher read the diff the same day and produced a crashing PoC on its own, before any advisory text named the affected module. The patch window for this bug, the time between “fix is public” and “exploit is reproducible by someone watching commits”, was zero days.
That's the other half of what AI changes about vulnerability research, and it cuts the opposite direction from everything above. AI made finding and developing this exploit cheaper for us; it made reproducing the bug cheaper for everyone watching commits. Those two facts together collapse the patch window from both ends. Coordinated disclosure assumes a gap between fix and weaponization that is now an automation target.
Writeup and PoCs: https://github.com/califio/publications/tree/main/MADBugs/nginx-CVE-2026-27654.
—anas, ryan, thai
