Repository navigation
fix(server): harden the console Host/Origin guards and fuzz them - #4
Merged
Merged
Conversation
Three findings from fuzzing the console, all in the guard rather than in a module — which is where a single mistake exposes everything at once. validHost trusted net.SplitHostPort, which validates the shape of an address but not its contents: it returns host "127.0.0.1" and port "8080/evil" for "127.0.0.1:8080/evil", and the same for "127.0.0.1:@evil", "localhost:80?x=1" and "127.0.0.1:8a08". The guard accepted all four. The stdlib HTTP server rejects those Host headers before a handler runs, so this was hardening and not a live bypass — but the DNS-rebinding control was deciding differently from the string it was handed, and now the port must be numerically valid or the value is refused. allowedOrigin checked only the parsed host, and Go's url.Parse puts the userinfo in u.User: "http://evil.com@127.0.0.1" parses with u.Host == "127.0.0.1", so a remote origin read as local. Browsers never send userinfo in Origin, so again this was not exploitable — it becomes one the moment the check is reused. An accepted Origin is now exactly scheme://loopback-host[:port]: no userinfo, no path, no query, no fragment. The guards now have four fuzz targets and two invariant tests: - FuzzValidHost / FuzzAllowedOrigin, both checked against an independent oracle that restates the policy naively ("only canonical loopback forms, optionally with a numeric port"). Verified to reproduce all three findings above on the pre-fix code from the seed corpus alone. - FuzzRoutesRejectBadToken drives the whole mux with an arbitrary Host, Origin and body and a token that cannot match by construction, so it only ever exercises refusals. - FuzzEqual pins token comparison to exact equality (prefixes, truncations). - TestGuardedRoutesRejectBadToken walks the route table in fuzz_test.go: a route registered without s.guard(...) fails it, which is how a module gets exposed by accident. TestRouteTableMatchesServer fails if the table drifts from routes(). Coverage 44.7% -> 45.2%, floor 42 -> 43. fuzz-smoke now runs nine targets.
21 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #2: the last unfuzzed surface, which is the one that matters —
the console's guard. Rainy-day findings, and I want to be precise about how
exploitable they were, because overstating them would be the same failure as
hiding them.
Findings
🟢
validHosttrustednet.SplitHostPort— it validates the shape of anaddress, not its contents:
So
validHostreturned true for127.0.0.1:8080/evil,127.0.0.1:@evil,localhost:80?x=1and127.0.0.1:8a08. The stdlib HTTP server rejects thoseHostheaders (httpguts.ValidHostHeader) before a handler ever runs, so thiswas not a live bypass — but this is the DNS-rebinding control, and it was
deciding differently from the string it was handed. The port must now be
numerically valid or the value is refused.
🟢
allowedOriginread a remote origin as local — Go'surl.Parseputsuserinfo in
u.User:The guard only inspected
u.Host, so it accepted it. A browser never sendsuserinfo in
Origin, so this was not exploitable either — it becomes onethe moment the check is reused or moved in front of something else. An accepted
Originis now exactlyscheme://loopback-host[:port]: no userinfo, no path,no query, no fragment.
🟢
originis not validated for a trailing-slash-free form — folded intothe same fix;
http://127.0.0.1/#xandhttp://127.0.0.1/?a=bwere accepted.No finding is CRITICAL or HIGH, and I would rather say that plainly than pad the
list. What made these worth fixing is that they are all in the guard, where a
single mistake exposes every module at once.
Tests with teeth
Four fuzz targets plus two invariants:
FuzzValidHost/FuzzAllowedOrigin, each checked against an independentoracle that restates the policy naively (only the canonical loopback forms,
optionally with a numeric port). An oracle that shares an implementation with
the code under test proves nothing, so it is deliberately dumb.
FuzzRoutesRejectBadTokendrives the whole mux with an arbitraryHost,Originand body, and a token that cannot match by construction (a byteis prepended). It therefore only ever exercises refusals — it can never reach
a state-changing handler, so it is safe to run unattended.
FuzzEqualpins token comparison to exact equality: prefixes, truncations andcase variants must never compare equal, since one
Equalcall gateseverything.
TestGuardedRoutesRejectBadTokenwalks the route table and fails if a route isregistered without
s.guard(...)— the realistic way a module gets exposed byaccident.
TestRouteTableMatchesServerfails if the table drifts fromroutes().I verified the targets are not vacuous by reverting both fixes: the seed corpus
alone reproduces all of them (and only them) —
Fuzzing results
No crashers, no stray
testdata/artifacts.Also
fuzz-smokenow runs nine targets (30s for the parsers, 20s for the consoleones, which are cheaper per execution and carry hostile seeds).
validHost,isPort,guardat 100%), floor42 → 43.
gofmt,go vet ./...,GOOS=linux go vet ./...all clean.Refs #2.