Skip to content

gix-url authority is not terminated by '?'/'#', causing the gix-transport redirect identity guard to fail open and leak HTTP Basic credentials

High
Byron published GHSA-jrcm-326h-gpp8 Aug 3, 2026

Package

cargo gix-transport (Rust)

Affected versions

<= 0.49.0

Patched versions

>= 0.58.1
cargo gix-url (Rust)
<= 0.32.0
>= 0.37.1

Description

Summary

gix-url's hand-rolled URL parser does not treat ? or # as terminating the authority.
For http://a?@b/repo it reports the host as b, where Git, libcurl, browsers and the
url crate all report a.

This is a correctness regression on its own. Its security consequence is that
gix-transport's HTTP redirect guard — redirect::can_reuse_identity(), added in PR #2686
to fix GHSA-9857 — compares the wrong host and fails open. A server that can return a
crafted Location gets the caller's Authorization: Basic credentials on the next request,
sent to an authority the credentials do not belong to.

Details

Root cause (#186) — gix-url/src/simple_url.rs:72

let path_start = after_scheme.find('/').unwrap_or(after_scheme.len());
let authority = &after_scheme[..path_start];
...
if let Some((user_info, host_port)) = authority.rsplit_once('@') { ... }

The authority is taken as everything up to the first /, so ? and # are absorbed into it,
and rsplit_once('@') then hands everything after the last @ to parse_host_port().
RFC 3986 §3.2 ends the authority at the first /, ? or #.

This is a regression from commit b34d1d270 ("feat: Replace url dependency with minimal custom
implementation", 2025-11-22). Before it, parse::url() took host from url::Url::host_str(),
which is RFC-conformant.

Url::to_bstring() percent-encodes ?/# into the userinfo, so a URL that gitoxide parsed
itself stays internally consistent. The divergence only bites where a raw URL string reaches
both gitoxide's parser and the HTTP client — which is exactly the redirect Location header.

Impact (#187) — gix-transport/src/client/blocking_io/http/redirect.rs:51

can_reuse_identity(redirect_url, original_url) calls gix_url::parse on the server-supplied
Location and compares .host. With a Location of the form
<attacker-authority>?@<original-authority><original-tail>:

  • redirect::base_url() accepts it (scheme_is_safe passes, the tail matches), so
    Transport::url becomes the attacker's base;
  • can_reuse_identity() reads the host as the original host, returns true, and
    sync_redirected_base_url_from() (http/mod.rs:262) therefore keeps the identity;
  • the next request() builds the POST URL from the attacker base and
    add_basic_auth_if_present() (http/mod.rs:306) attaches Authorization: Basic <creds>.

libcurl does the right thing on its own — it strips the custom Authorization on the
cross-origin GET redirect. gitoxide then re-attaches it on the POST.

Reachable with default configuration: http.followRedirects defaults to
FollowRedirects::Initial (http/mod.rs, #[default] Initial).

Reproduction

Two loopback HTTP servers, no external hosts. Confirmed on main @ 5708de434,
macOS, rustc 1.95.0, system git 2.50.1.

(a) Host divergence, via the gix CLI

http://localhost?@127.0.0.1:8802/repo — a server listening on 127.0.0.1:8802 logs the request
from gix clone, while git clone of the same string reports
Failed to connect to localhost port 80 and the 8802 server sees nothing. The url crate
reports host=Some("localhost"). Identical result with # in place of ?.

(b) Credential leak

A driver built against gix-transport (features blocking-client, http-client-curl, http-client-curl-rust-tls, http-client-insecure-credentials) sets an identity and handshakes
against http://127.0.0.1:8801/repo. The 8801 server answers the info/refs GET with a 302
whose Location uses the ?@ shape above and points at 127.0.0.1:8802. The 8802 server then
logs a POST carrying Authorization: Basic with the credentials that were provisioned for
8801, and the transport reports identity after handshake: Some("victim-user").

Note on the repro: http-client-insecure-credentials is only needed because
add_basic_auth_if_present() refuses cleartext HTTP. On https:// — the real-world case —
there is no such gate and no feature flag is involved; scheme_is_safe() permits
https → https, so the same Location works.

Full server scripts and the driver source are held locally and will be provided on request
rather than published.

Impact

An attacker who can control a redirect response from an origin the user authenticates to
(a hostile or compromised forge, or an open redirect on a legitimate one) obtains that origin's
HTTP Basic credentials — commonly a PAT or OAuth token from the user's credential helper.

Independently of the credential leak, any embedder that validates or displays a repository URL's
host with a conformant parser and then hands the same string to gix will disagree with where
gitoxide actually connects. That is a host-allowlist bypass / request-forgery primitive for
downstream consumers of gix-url.

Suggested direction (maintainer's call)

  1. In simple_url::parse, end the authority at the first of /, ?, #, and keep the
    remainder in path as today. This restores the pre-b34d1d270 host and is the narrow fix.
  2. Independently, can_reuse_identity() should fail closed on any URL whose authority contains
    a character that is not legal there, rather than trusting a permissive parse of a
    server-controlled header.
  3. Extend redirected_post_does_not_forward_basic_auth_to_the_new_host with the ?@ and #@
    Location shapes — the current test only exercises a plain cross-host Location.

Happy to open the PR once you have decided how you want this handled and disclosed.


Disclosure: this report was prepared with AI assistance (Claude Code) operating through
the shuvamk account, per CONTRIBUTING.md's "Prevent agent impersonation" section. Every
result quoted above was executed locally against the commit named; the host-divergence and
redirect code paths were re-verified unchanged on main @ da5fa7359 before sending.

Severity

High

CVE ID

No known CVE

Weaknesses

Insufficiently Protected Credentials

The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval. Learn more on MITRE.

Improper Validation of Syntactic Correctness of Input

The product receives input that is expected to be well-formed - i.e., to comply with a certain syntax - but it does not validate or incorrectly validates that the input complies with the syntax. Learn more on MITRE.