Meet Keith, and why we're keeping it closed
If you want to find bugs in HTTP parsers, the last thing you want is a good HTTP library. That sounds backwards, so let me explain, because it is the whole reason Keith exists.
A request-smuggling bug lives in a disagreement. The front-end proxy and the back-end server read the same bytes and reach different conclusions about where one request ends and the next begins. To trigger it you have to put a malformed request on the wire: two Content-Length headers, a header line ended with a bare newline instead of a proper carriage-return-newline, a Transfer-Encoding value with a sneaky tab in it. Those malformations are the test.
Here is the problem. Every decent HTTP client quietly repairs all of that before it reaches the network. Give a popular library two Content-Length headers and it collapses them into one. Give it a Transfer-Encoding it dislikes and it normalises or refuses it. The library is doing its job, being a conformant, helpful client. But helpful means it deletes your test case, and you end up auditing the library's manners instead of the server's parser.
So we wrote our own engine, and its defining feature is that it is not helpful. We call it Keith.
Byte-exact, no normalisation. Keith stores a request as raw bytes, not a tidy typed structure. Two Content-Length headers stay two headers, in order, on the wire, because which one the front-end honours and which one the back-end honours is the whole game. You can end one header line with a bare newline and the next with a proper CRLF. Nothing gets collapsed, re-cased, deduplicated or "fixed." What you write is what goes out. That discipline runs all the way up the stack. For HTTP/2 and HTTP/3 the header-compression layers (HPACK and QPACK) are the usual normalisers, because a compliant encoder simply will not emit a header that is illegal in that protocol, so Keith hand-rolls both encoders and passes names and values through byte for byte. The one thing we do not rebuild is the transport: TLS, packet-loss recovery, the QUIC machinery. We borrow that from a real library, the same way an old HTTP/1 tool borrows the kernel's TCP. The rule we keep coming back to is short: borrow the transport, own the framing.
No bare booleans. Keith never reports "vulnerable" or "safe" as a naked verdict. Every finding carries the evidence that justifies it: the baseline timing against the probe timing, the response statuses, the byte counts. A discrepancy you cannot show your working for is a false positive waiting to embarrass you. So the verdict has four states (discrepancy, clean, inconclusive, error) and each one drags its measurements along behind it. If a probe is merely slow rather than genuinely hung, Keith calls it inconclusive, not a finding. Measure before you claim.
Safe by default. Keith's default mode prints the exact bytes it would send and sends nothing at all. Building a probe and firing a probe at someone are deliberately different acts. Going live means naming the target twice, with an explicit flag that has to match the host you are pointing at, and only ever against infrastructure you are allowed to test. You can build and review the most aggressive probe in the world without a single packet leaving your machine.
Under the hood it stays small on purpose: a from-scratch engine for HTTP/1.x, /2 and /3 with no external dependencies, the whole thing test-driven, every probe class round-tripped and asserted byte for byte. Small and auditable is the point. A tool you use to make claims about other people's parsers had better have a parser you can fully trust yourself.
Why we're keeping it closed. We started this as a build-in-public log, and the unspoken assumption was that Keith would end up on GitHub like everything else we ship. We have changed our minds, and I would rather say so plainly than let the repo quietly never appear.
The thing that makes Keith safe is not in the code. It is the discipline wrapped around it: passive recon, a lab of proxies we own, a back-end that logs the literal forwarded bytes, an authorisation step before a single live packet, and coordinated disclosure when something real turns up. None of that ships with the binary. What does ship is a frictionless engine for putting precisely malformed framing on the wire against any host you point at. For someone doing this work properly, that engine is a convenience they could rebuild in a week. For everyone else it is a loaded tool with the safety filed off. The trade did not come out in favour of release.
So Keith stays in-house. We will keep publishing what we find with it: the method, the clean negatives, and the occasional real bug taken through coordinated disclosure. The findings travel. The tool stays home.
The two earlier dispatches, the day-0 note on byte-exact request crafting and the deep dive on the HTTP/3 FIN desync, are the inside view of building this. This is the outside view: Keith is an HTTP engine built to be everything a good client refuses to be, so that the malformation survives long enough to tell us something true.
Simon Morley researches infrastructure security and is the founder of NullRabbit. About / contact.
Related Posts
Keith, day 0: byte-exact or bust
Starting a build-in-public log for Keith, an HTTP/1.1/2/3 desync prober. The premise: a conformant HTTP client is the wrong tool for finding HTTP parser bugs, because it normalises away exactly the malformed framing you need to send.
How we hunt request smuggling without breaking anything
A timing hunch is not a finding. The discipline that separates real desync research from noise is the part nobody photographs: a lab of real proxies, a back-end you own that logs the literal forwarded bytes, and a hard line about who you're allowed to point any of it at.
The h3 FIN/EOM desync, and why your smuggling tool can't send it
HTTP/3 request smuggling is almost unploughed ground. Not because the surface is small, but because nearly every tool speaks h1/h2 only, and the few that speak h3 do it through a conformant QUIC library that won't let you send the bug.
