BaseURL on the source detail page uses the raw X-Forwarded-Proto value as the URL scheme #272
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the TLS-detection audit in #269, which asked for a third site that decides "is this request TLS?". This is it, and it is the worst-behaved of the three.
internal/handlers/source_management.go:432-440:The header value is not parsed, not normalized and not validated — it is assigned straight into the scheme, and the result is concatenated into
BaseURL(scheme + "://" + host) and rendered on the source detail page as the entrypoint URL an operator copies into the sending system.Consequences, using the same spellings probed in #269:
X-Forwarded-Proto: HTTPSproducesHTTPS://host/...X-Forwarded-Proto: https, httpproduceshttps, http://host/...X-Forwarded-Proto: https,httpsproduceshttps,https://host/...http/https— lands in the scheme verbatim.Note also the first branch is dead in the common case: when the header is present at all it overwrites whatever
r.TLSdecided, so a direct-TLS request behind a proxy that setshttpis displayed ashttp.#269 deliberately did NOT fix this —
internal/handlerswas held by a parallel unit at the time and the scope wasinternal/sessionplusinternal/middleware. It landed the shared predicate this should adopt.Definition of done
internal/handlers/source_management.goderives the scheme fromreqtls.IsTLS(r)(added by #269,internal/reqtls) rather than from the raw header, so the scheme can only ever behttporhttpsand all three call sites agree.BaseURLishttps://hostfor each ofhttps,HTTPS,https, http,https,https, andhttp://hostforhttp.make checkgreen.Safety question settled during the review of #276, since the raw header lands in a rendered URL.
No XSS, and no dangerous
href. But the protection is not what you would guess:html/template'surlFilternever runs.templates/source_detail.html:73renders{{$.BaseURL}}/webhook/{{.Path}}inside a<code>element — HTML text context, not anhrefor any URL attribute — so a hostile scheme token gets ordinary entity escaping and appears as inert text.static/js/app.js:33copies it withtextContent, which navigates nowhere.Two things that follow, both worth handling in this issue:
BaseURLinto anhrefchanges the analysis. It would still be safe, via#ZgotmplZ, but for an entirely different reason. Put a short comment at the assignment site saying the value is unvalidated and safe only because of where it is rendered — that is exactly the kind of trap a comment should mark.host := r.Hoston the adjacent line is equally unvalidated and fully client-controlled — a larger lever than the scheme, with the same benign outcome in this context. Fixing the scheme while leaving the host unvalidated addresses the smaller half. Decide deliberately whether to constrain the host too, and say which you chose.Confirms the severity already recorded here: a correctness defect, because the operator copies a wrong URL into the sending system and the webhook never arrives.
Plan, on branch
issue-272-baseurl-scheme:r.TLS/raw-header scheme derivation inrenderSourceDetailwithreqtls.IsTLS(r)(http/httpsonly). No parallel parse.r.Hostunconstrained, deliberately. There is no canonical hostname anywhere in config to validate against, so a shape check could only reject with nothing correct to fall back to, and any constraint risks breaking exactly the deployments that matter (proxy on a non-default port, IPv6 literal). The reasoning goes in the PR body.templates/source_detail.html:73renders it in HTML text context inside<code>; moving it into anhrefchanges that.internal/handlersdriving the real handler and asserting the rendered entrypoint URL forhttps,HTTPS,https, http,https,https,https(trailing space),http, a garbage token, and no header at all.PR: #286
Scheme now derives from
reqtls.IsTLS(r), so it is only everhttporhttps, and the dead branch is gone: direct TLS outranks a header claiming plaintext. A garbage token rendershttp://host— not TLS by the shared predicate, and never the token.Host: left unconstrained, deliberately. No canonical hostname exists in config to check against, so a check could only reject with nothing correct to fall back to; the page is authenticated,
no-storeand rendered back to the same requester, so nothing persists a poisoned value; and constraining it would break proxy-on-a-non-default-port and IPv6-literal deployments. Comment at the assignment site marks that the value is inert only because of the<code>render context.Verified: probe written first, failing on unmodified
nextforHTTPS,https, http,https,https,https, the garbage token and the TLS-precedence case; green after.make checkgreen withGOFLAGS=-count=1, tests uncached, lint in Docker at 0 issues. Also a live instance onhooks.example.com:19520—X-Forwarded-Proto: HTTPSrenderedHTTPS://hooks.example.com:19520/webhook/...before andhttps://hooks.example.com:19520/webhook/...after, with the port preserved; plain HTTP with no header rendershttp://....