security: consolidate the HTTP clients through one hardened builder (the config client leaks a credential on redirect) #708
Labels
No labels
breaking
bug
documentation
enhancement
epic
good first issue
help wanted
refactoring
resolution/duplicate
resolution/invalid
resolution/wontfix
security
severity/critical
severity/high
severity/low
severity/medium
status/abandoned
status/blocked
status/needs-decision
status/needs-info
status/needs-verification
testing
upstream
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
sixfold-space/madtea#708
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
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?
What happens
madtea builds three HTTP clients independently, and only one carries the redirect guard.
internal/gitea/client.gobuilds throughbuildHTTPClient, which wires aCheckRedirectthat strips theAuthorizationheader on any cross-origin or scheme-downgrade redirect hop, and stops a method-changing redirect. Hardened.internal/config/config.go:61builds a bare&http.Client{Timeout: ...}with noCheckRedirect. It carries a credential on three call sites:internal/config/request.go:36- thetokenheader onwhoamiand the pre-trust probes.internal/config/oauth_mint.go:76-79- the OAuth token via basic-auth.internal/config/oauth.go:223-230- the OAuth code-exchange POST (code + verifier).internal/cmd/setup/prompt.go:168also builds a bare client, but its one request is a token-lessHEAD, so it carries no credential.net/http re-attaches
Authorizationacross a redirect to the same host and ignores a scheme downgrade, so a same-hosthttps -> httpredirect from the configured forge sends the token (or the OAuth token) in cleartext. The OAuth POST also resends its body on a 307/308. The gitea client is immune; the config client is not.Scope (full sweep)
Exactly three production
http.Clientconstructions exist (giteaclient.go, configconfig.go:61, setupprompt.go:168). Nohttp.DefaultClient, no custom transport elsewhere. Three clients, one policy, is the whole surface. (The sweep also found unrelated items filed separately: a missing ref check ongit tag <name>attag.go:173, a raw path-concat inclient_repo_flags.go, and the OAuth client-id legacy-key fallback.)Root cause
There is no shared HTTP module.
internal/configis the base layer - the API client is built from the credentials it resolves - so it cannot importinternal/gitea, and it hand-rolled a bare client. The redirect guard was written into the gitea client alone.Decision grounding
This finishes the logic of two accepted records; it does not fork them. ADR 0041 ruled that the token-security check belongs at one chokepoint, no opt-out, but scoped it to
internal/config's request function and never reckoned with the separate gitea attach path or the redirect dimension. ADR 0025 section 5: a credential attaches only to its authorized origin - the redirect strip is the per-hop half of that. Credential resolution stays ininternal/config(ADR 0025 / 0027); the new module owns transport only. The transport-module decision is filed as aproposedADR for second-contributor assent (ADR 0047), not landed asacceptedby one author.Fix
A new base package
internal/httpxowns the hardened builder (redirect credential strip, method-preserving redirect stop, 64 MiB body cap, same-origin check) and imports no other madtea package.internal/gitea,internal/config, andinternal/cmd/setupall build throughhttpx.Build; the config client's three call sites inherit the strip with no change to how they attach the token.Acceptance criteria
http.Clientis built throughinternal/httpx.https -> httpsame-host redirect, and stops a POST redirect.http.Client,http.Get, orhttp.DefaultClientis used outsideinternal/httpx../scripts/gate.shis green.proposed.Part of v0.20.0 (#675). The only behavior change for a legitimate user is a credential stripped on a malicious downgrade redirect - intended.
Landed in #709 and accepted as ADR 0049 (0049-one-hardened-http-client-layer).