#2382·go-git

HTTP transport: a few attacker-controlled strings bypass bounded()/redactURL in error messages

Author: stysusCreated Sep 11, 2026Updated Sep 11, 2026

Follow-up to #2380/#2381. While auditing plumbing/transport/http we found three spots where a server/redirect-controlled string is embedded into an error message without going through the bounded()/redactURL machinery this package otherwise uses consistently to cap attacker-controlled input before rendering it into errors/logs. These aren't fetch-breaking — they're a hardening/consistency gap relative to the invariant the package already establishes elsewhere.

1. Content-Type header embedded unbounded in describeInfoRefsError

plumbing/transport/http/handshake.go:336: mediaType (derived from the response's Content-Type header via contentMediaType, which has no length cap) is embedded via %q into the returned error, unlike every other server-controlled string this package renders through bounded().

A server or redirect target sending an oversized Content-Type header (bounded only by net/http's own multi-MB header size limit) gets that value embedded verbatim into the error message and any log/trace that prints it.

2. redactURL doesn't bound u.Scheme

plumbing/transport/http/common.go:578: redactURL bounds Host, Path, RawQuery, Fragment, and User, but never bounds or redacts Scheme, even though net/url.Parse places no length limit on it.

A misbehaving redirect target can send a Location header with an oversized scheme-like prefix (e.g. thousands of legal scheme characters before ://). When a hop is refused (e.g. under NoFollowRedirects, or a scheme/host change checkRedirect rejects), redactClientError/redactedURL passes that oversized scheme straight through unbounded into the error message the caller receives — unlike every other URL component this function is designed to cap.

3. applyRedirect error branches print unbounded path/scheme

plumbing/transport/http/common.go:247 (and ~250, ~265, ~269-271): several error branches print server-controlled finalPath and final.Scheme/baseURL.Scheme directly via %q, without routing them through bounded() — unlike the analogous "unsupported scheme" check in checkRedirect (http.go), which does call bounded().

A redirect target whose path doesn't end in /info/refs, or whose scheme isn't http/https, causes the full unbounded attacker-supplied path/scheme string to be embedded in the returned error — inconsistent with the package's own stated invariant that a rendered refusal must never exceed maxRedactedComponent.

Suggested fix

Route all three through the existing bounded() helper (or redactURL, where a full URL is being rendered), consistent with how every other server-controlled string in this package is already handled.


Disclosure: these findings came from an AI-assisted code review (Claude) of this package, run alongside the investigation for #2380/#2381. The core claims were manually spot-checked against the current source before filing. Happy to provide more detail if useful.