Repository navigation
fix(relay): backport HTTP response and ops-listener hardening - #7920
Conversation
Backport of #7210 to release. - /fetch serves bytes as application/octet-stream; /fetch and /announced send X-Content-Type-Options: nosniff and Cache-Control: private, no-store, refusals and extraction errors included. - The internal (ops) listener refuses any request carrying Origin, and any request whose Host or URI authority is not the accepted socket's local IP and port (localhost only on loopback), before dispatch so merged routes inherit it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: beeb23d
No actionable bugs found. The backport preserves #7210's response hardening and pre-dispatch ops guard while adapting to release's TcpStream listener API. The documented DNS/NAT restriction remains intentional. The focused backport is a reasonable, simpler approach than changing the release listener abstraction.
Static GitHub review only; I did not run tests. CI was still in progress when checked.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Ops listener admission:
InternalAcceptorreads the accepted socket'slocal_addr()and wraps the final router inInternalService, so all requests (including embedder-merged routes) are checked before dispatch. - Origin and authority policy:
allows_requestrefuses anyOriginand requires every presentHost/URI authority to name the socket;owns_authoritynormalizes IPv6/IPv4-mapped forms, acceptslocalhostonly on loopback, and rejects userinfo, bad ports, and mismatched hosts. - Public HTTP headers:
/announcedand/fetchmoved into agrantedsub-router with amap_responselayer addingCache-Control: private, no-storeandX-Content-Type-Options: nosniff;ServeGroupnow sendsContent-Type: application/octet-stream. - Tests and docs: ported the unit/HTTP2/HTTP1 tests, the web header/body test, and the
http.mddocumentation.h2dev-dependency comment updated.
The port matches main's #7210 logic exactly, with only the documented release API substitutions. I checked the authority parsing against the http/hyper/h2 sources: they share the same Authority grammar, and the policy requires all present authorities to pass, so there is no parser-disagreement fail-open. The DNS/service-name deployment tradeoff is already documented in the PR and http.md.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Problem
On
release,/fetchstreams publisher bytes with noContent-Type, so a browser can sniff them as active content on the relay origin, and the authenticated/fetchand/announcedresponses carry no cache policy. The unauthenticated ops listener accepts browser-origin POSTs (a cross-site form tohttp://127.0.0.1:9101/sessions/revalidate) and reads through a DNS-rebinding hostname.Tracking quest: quest/m0/backport-release-hardening.md, claimed in moq-dev/moq#7596.
Approach
Hand-port moq-dev/moq#7210 (merge commit 21112b33a6).
/fetchserves bytes asapplication/octet-stream./fetchand/announcedsit in their own sub-router whosemap_responselayer addsX-Content-Type-Options: nosniffandCache-Control: private, no-store, refusals and extraction errors included.Origin, and any request whoseHostor URI authority is not that local IP and port.localhostis accepted only on loopback, dual-stack::ffff:addresses are canonicalized, HTTP/2 authority is checked, and missing, duplicate, or malformedHostis refused. It runs before dispatch, so routes an embedder merges in inherit it.doc/bin/relay/http.mdand theinternal.rsmodule and method docs. Skip its quest-file changes.Differences from main, all from
release's APIs rather than behavior:release's listener yields a plaintokio::net::TcpStream, soInternalAcceptorimplementsAccept<TcpStream, _>and takeslocal_addr()from the stream directly (nolistener::Acceptedwrapper). The web test usescreate_track(name, None), two-argumentauth::Config::init, andConnectInfo<listener::Peer>.Impact
/fetchand/announcedresponses gainCache-Control: private, no-storeandX-Content-Type-Options: nosniff;/fetchgainsContent-Type: application/octet-stream.localhoston loopback. DNS aliases, forwarded caller hosts, and NAT or port-mapped addresses are refused.moq auth sessions/revalidateand the documentedcurl http://127.0.0.1:9101/...examples already comply; nothing underinfra/orpackaging/reaches this listener.Alternatives
A cherry-pick would drag main's
listener::Acceptedandlistener::Configintorelease. A bearer token was excluded by the settled quest because the listener stays on a trusted plane; CORS alone cannot stop a form POST.ConnectInfowas not used because axum_server's connect info carries only the peer address, not the local one.Choices made without asking
servecomment "No accept-time work" to say the acceptor only reads the local address, since the new acceptor now does that much.h2dev-dependency comment inrs/moq-relay/Cargo.tomlto name the new internal-listener test as a user, alongside thehttp_authbench.headerhelper so a missing header fails asleft: Nonewith the path, rather than anunwrappanic.Validation
All on head
beeb23df749ad34b0c0852db5d52944e1851be57, in/home/kixelated/.worktrees/backport-relay-http-hardening/moq(outside the main checkout, sorelease's own.cargo/config.tomlapplies), each throughquest gate nix develop --command ...:authority_matches_only_the_local_address_and_port, unit-tests the newowns_authorityhelper and so cannot compile without it):internal_requests_require_the_listener_authority_and_no_origin:listen=127.0.0.1:0, POST /sessions/revalidate, "Host: 127.0.0.1:42777\r\nOrigin: https://evil.example\r\n": HTTP/1.1 200 OK(expected 403).internal_http2_checks_authority_without_a_host_header:evil.example, host=None, origin=Nonereturned200, expected403.authenticated_http_responses_are_private_and_cannot_be_sniffed: refused/fetch/room/videohadCache-ControlNone, expectedSome("private, no-store").serve_hosts_merged_routes_alongside_the_defaults.just ci check origin/release: pass (rs: selecting moq-cli moq-relay), including clippy, rustdoc, the io_uring clippy, JS check/build, actionlint, andnix flake check.just ci test origin/release: pass (rs: selecting moq-cli moq-relay). Rust: 379 passed, 2 skipped, plus 133 passed (moq-cli). JS suites pass.just rs uring(includesjust rs uring-check): pass. 337 passed, 2 skipped (moq-uring and moq-relay withio-uring), plus 78 passed (moq-uring withqlog).sh/api-diff/run.sh --deny rust(main's script;releasedoes not carrysh/api-diff/) againstorigin/release: pass, all 30 published cratesunchanged, includingmoq-relay0.17.12.quest check --base origin/release: pass.Review
Codex,
gpt-6.1-sol, medium effort, round 1 (quest review --base origin/release). The author is Claude, so Codex is the other vendor. Reviewed head:beeb23df749ad34b0c0852db5d52944e1851be57. No findings.Follow-ups
[internal]host allowlist.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code