CVE-2026-3783: token leak with redirect and netrc
Medium
Vulnerability Details
##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`):
```c
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`):
```c
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`):
```c
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`):
```c
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`):
```c
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:
```bash
echo "default login testuser password testpass" > /tmp/test-netrc
```
Save as `redirect_servers.py`:
```python
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):**
```bash
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):**
```bash
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):**
```bash
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:
```c
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:
```c
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.
Actions
View on HackerOneReport Stats
- Report ID: 3583983
- State: Closed
- Substate: resolved
- Upvotes: 2