fix(ext/http): treat empty Host header as missing in request URL#33234
fix(ext/http): treat empty Host header as missing in request URL#33234jeanibarz wants to merge 1 commit intodenoland:mainfrom
Conversation
An empty `Host:` header reached `req_host` as `Some("")` and was pushed
through to JS as an empty authority, which the URL builder in
`00_serve.ts` combined with the scheme and path to produce either
`http:///` (fails `new URL()`) or, worse, `http:///path` — which parses
as `http://path/`, silently promoting the path component to the URL
hostname. Treat empty Host values as if no Host header was sent so the
listener's fallback authority is used and `request.url` is always a
parseable URL that identifies the actual server.
Covered by a new row in the `createUrlTest` table in
`tests/unit/serve_test.ts`.
bartlomieju
left a comment
There was a problem hiding this comment.
Thanks, can you add a test?
|
Thanks for taking a look! A regression test is actually already included in this PR — The helper itself also needed a small tweak ( Happy to restructure it if you'd prefer a different shape (e.g. a standalone |
Fixes #29872.
Problem
When a client sends an HTTP/1.1 request with an empty
Host:header,Deno.serve()produced arequest.urlthat was either unparseable or — worse — silently promoted the URL path to the hostname:GET / HTTP/1.1\r\nHost:\r\n\r\n→request.url = "http:///"→new URL(request.url)throwsTypeError: Invalid URLGET /path HTTP/1.1\r\nHost:\r\n\r\n→request.url = "http:///path", whichnew URL()parses ashttp://path/. The path component becomes the URL hostname, which is a correctness and security concern for handlers that route onnew URL(request.url).hostname.The original report (#29872) hit the first case on Deno 2.1.6 via a production server receiving a bot probe. The naked IP form (
90.151.171.106:443) shown in the issue stack trace is now rejected by hyper as 400 before reaching the handler, but the underlying invariant — "request.urlis always a valid URL that identifies the actual server" — was still broken via the empty-Host path.Root cause
ext/http/request_properties.rs::req_hostreturnedSome("")for an emptyHost:header. The op inhttp_next.rsforwarded that empty string verbatim to JS, andext/http/00_serve.tsused??to coalesce the authority with the listener's fallback host — but??does not coalesce empty strings, so""reached the URL builder and producedhttp://+""+/path=http:///path.Fix
Treat an empty
Host:header value as if no Host header were sent, soreq_hostreturnsNoneand the listener's fallback authority (already computed at listen time) is used. Hyper's HTTP/1 parser already trims OWS from header values, so whitespace-onlyHost:values arrive here as empty bytes and are covered by the same check.The change is scoped to one function. The
??coalesce in00_serve.tsis unchanged and still load-bearing for the case where no Host header is sent at all.Verification
Reproduced the bug locally, applied the fix, rebuilt
deno, ran the new test against both versions:Before the fix (
httpServerUrlWithEmptyHostHeaderadded to the existingcreateUrlTesttable):After the fix:
Also ran an end-to-end probe via raw TCP against a patched
Deno.serve:The broader
tests/unit/serve_test.tssuite passes (160/162; the 2 failures arehttpServerVsock*tests that require kernel vsock support unavailable in my WSL2 environment, unrelated to this change).Test
Added
WithEmptyHostHeaderas a new row in the existingcreateUrlTesttable intests/unit/serve_test.ts. The helper previously usedhost ? ...which conflatednull(omit header) with""(sendHost:with empty value); changed tohost !== null ? ...so the two cases are distinguishable. Existing rows continue to usenulland are unchanged.Checklist
cargo build --bin denosucceedscargo clippy -p deno_http -- -D warningscleandprint checkclean on the two changed files (with the pinned toolchain's rustfmt for the Rust file)deno lintclean on the test filehttpServerUrl*tests pass (14/14)Deno.serve()'srequest.urlmay not be a valid url #29872)AI disclosure
I used Claude (an AI assistant) to help investigate the code path, draft the fix, and write the regression test. I reviewed every change, reproduced the bug locally, and verified the fix end-to-end before opening this PR.