curl · Report #3973127
18: Explicit IPv6 proxy zone ID silently ignored — proxy credentials sent to wrong interface
How it was found
Explicit IPv6 proxy zone ID silently ignored — proxy credentials sent to wrong interface
Summary
When a proxy is configured with a literal link-local IPv6 address and an explicit zone ID (e.g. http://[fe80::1%25eth0]:8080), curl parses and stores the zone correctly but never applies it to the socket. On a multi-homed host, get_scope_id() falls back to the first link-local interface it finds, which may be a different one. This causes proxy credentials (Proxy-Authorization) to be sent to an unintended network endpoint.
This is a regression introduced by commit bc40e09f63 ("lib: introduce Curl_peer", 2026-05-05). Before that refactor, zonefrom_url() in parse_proxy() placed the proxy's zone ID into conn->scope_id. The refactor deleted zonefrom_url() and moved zone storage to peer->scopeid, but conn->scope_id is now only set from CURLOPT_ADDRESS_SCOPE or the origin peer — never from the proxy peer. This regresses the fix from PR #3918 / issue #3482 ("parse_proxy: use the IPv6 zone id if given").
Affected version
curl 8.21.0, plus affected 8.22.0 prerelease/development versions. Introduced by bc40e09f63.
Root cause
-
peer_create()correctly converts the proxy URL zone ID toproxy_peer->scopeid(lib/peer.c:146-170). -
Curl_str2addr()/ip2addr()creates a zero-initializedsockaddr_in6without copying any scope ID (lib/curl_addrinfo.c:406-412). -
Before
connect(),get_scope_id()(lib/cf-socket.c:379-417) checks:conn->scope_id— set fromCURLOPT_ADDRESS_SCOPEor origin peer, not the proxy peer (lib/url.c:1566-1567)sa6->sin6_scope_id— always zero (see step 2)- First link-local interface via
getifaddrs()— picks whichever comes first
It never checks
Curl_conn_get_first_peer(conn, ...)->scopeid. The proxy's explicit zone is parsed, stored, and then silently discarded.
Security impact
On a host with multiple link-local interfaces where a different interface shares the same link-local address, curl connects to the wrong endpoint and sends proxy credentials there. The reproduction below proves disclosure of HTTP Basic Proxy-Authorization to an unintended listener. The same routing error would affect HTTPS CONNECT tunneling and SOCKS proxy authentication.
The configuration is narrow (literal link-local proxy, multiple eligible interfaces, same link-local address on the wrong interface), but the user explicitly specified the correct interface via the standard zone ID syntax and curl silently ignored it.
Reproduction
- Build curl 8.21.0 or another affected revision with IPv6 and proxy support enabled.
- Prepare a host or isolated network namespace with two active IPv6 link-local interfaces.
- Place a separate endpoint behind each interface and assign both endpoints the same link-local address and TCP port, such as
[fe80::1]:18080. - Ensure the unintended interface is enumerated before the intended interface so that curl's automatic link-local scope fallback selects the unintended one.
- Start a simple HTTP proxy listener on each endpoint. Have each listener record incoming connections, request lines, and any
Proxy-Authorizationheader. - Configure curl to use the proxy's literal link-local IPv6 address with an explicit zone identifying the intended interface, percent-encoding the zone delimiter in the URL, for example
http://[fe80::1%25intended0]:18080. - Configure HTTP Basic proxy credentials, clear any proxy-bypass configuration, and request an HTTP URL through the proxy.
- Record the scope ID used by curl and identify which listener receives the connection.
- Observe the vulnerable result: curl ignores the zone from the proxy URL, falls back to the unintended interface, and sends the proxy request and credentials to that endpoint. The listener on the explicitly selected interface receives nothing.
- Repeat the test with the numeric interface index in the proxy URL instead of its name. Confirm that the explicit numeric zone is ignored in the same way.
- As a control, perform the same transfer through libcurl while setting
CURLOPT_ADDRESS_SCOPEto the intended interface index. - Confirm that the control connects only through the intended interface and that the unintended listener receives no credentials.
- Optionally repeat the vulnerable case with an HTTPS proxy tunnel or a SOCKS proxy and verify that the same routing error exposes the corresponding proxy-authentication exchange to the unintended endpoint.
- Confirm a fix by making socket setup prefer the selected first peer's explicit scope ID before automatic interface fallback, while preserving an explicit
CURLOPT_ADDRESS_SCOPEas the highest-priority override. - Repeat the original test and verify that only the listener behind the zone specified in the proxy URL receives the connection and credentials.
Expected result
An explicit IPv6 zone in a proxy URL must determine the interface used to reach that link-local proxy. Curl must not send proxy traffic or credentials through another interface.
Vulnerable result
Curl parses and stores the proxy zone but does not apply it when opening the socket. Automatic scope selection can therefore route the connection and proxy credentials to an endpoint on a different interface.
Suggested fix
In get_scope_id() (lib/cf-socket.c), after checking conn->scope_id and before the getifaddrs fallback, check the first peer's explicit scope:
if(data->conn->scope_id)
return data->conn->scope_id;
/* Check the first peer's explicit zone (proxy or connect-to) */
{
struct Curl_peer *peer = Curl_conn_get_first_peer(data->conn, 0);
if(peer && peer->scopeid)
return peer->scopeid;
}
scope_id = sa6->sin6_scope_id;
/* ... existing fallback ... */
Impact
Summary:
An attacker controlling the same link-local IPv6 address on another interface may receive traffic intended for the explicitly selected proxy, including HTTP Basic credentials, HTTPS CONNECT authentication, or SOCKS credentials. This can disclose proxy secrets and potentially allow the attacker to intercept or manipulate proxied traffic.