First of the SonarCloud fixes (#153). The BLOCKER.
Both directory redirects in pages.go passed the raw request path to
http.Redirect, while the sibling reqPath a line above was already
cleaned. net/url keeps a leading //, and Go emits
Location: //evil.example/ unchanged — a browser reads that as
protocol-relative and follows it off-site. I confirmed it by calling
http.Redirect directly rather than reasoning about it.
Reaching it needed content named like a host under the subdomain's own
owner — a public repo with a pages branch, and repo names permit dots, so
evil.com is legal. Narrow rather than impossible.
Two bypasses my first fix still had, both caught by the test rather than by me:
- A path whose
..escapes the root cleans to/, and appending a slash produced//— the exact string being defended against. - A backslash is not a path separator here, but a browser following the
WHATWG URL rules treats one as a slash, so
/\evil.example/is another spelling of the same attack.
Building the destination through url.URL escapes both, and every other
byte a path can hold, without a denylist. The test asserts the property —
"the destination is a same-origin absolute path" — rather than the
spelling, so it survives a future rewrite of how the target is built.
Ref #153