# Variant Root Cause Analysis Report - CVE-2026-34441

## Summary

This variant analysis examined cpp-httplib for potential bypasses to the HTTP request smuggling fix and discovered a variant trigger using HEAD requests instead of GET. While the original vulnerability was demonstrated with GET requests to static file handlers, the same root cause affects HEAD requests equally - both methods bypass the `expect_content()` body consumption check when matching static file mount points. The fix in v0.40.0 correctly handles both GET and HEAD variants by implementing a body-draining mechanism that runs after all request processing. No bypass of the fixed version was found; all tested variants are properly mitigated by the fix.

## Fix Coverage / Assumptions

The original fix (commit 6fd97ae in v0.40.0) relies on the following key mechanisms:

1. **Body consumption tracking**: A new `body_consumed_` flag in the Request class tracks whether the body has been read
2. **Post-processing drain**: After request handling completes (whether successful or not), the code checks if the body was expected but not consumed, and drains it from the stream
3. **CL+TE rejection**: Requests with both Content-Length > 0 AND Transfer-Encoding headers are rejected with 400 Bad Request (per RFC 9112 §6.3)
4. **Connection closure on drain failure**: If draining fails (e.g., body exceeds payload limits), the connection is closed rather than reused

The fix explicitly covers:
- Static file serving (GET and HEAD via `handle_file_request()`)
- All routing paths including `pre_routing_handler` early returns
- Exception handling paths (exceptions in routing still flow through to the drain code)
- Error responses that return early (400, 414, etc. - these now set `connection_closed = true`)

The fix does NOT explicitly cover:
- WebSocket upgrade paths (but these set `connection_closed = true` before returning)
- Content-Length: 0 with Transfer-Encoding (tolerated per RFC for compatibility)

## Variant / Alternate Trigger

### Variant 1: HEAD Request Smuggling (Confirmed Variant)

**Entry point**: HEAD requests to static file mount points with Content-Length header

**Code path**: 
- `Server::routing()` at line 11629-11631 (v0.40.0)
- `handle_file_request()` returns true for HEAD requests, causing early return
- Same flow as GET: the `expect_content()` check is never reached

**Evidence**: 
- Test v3_head shows smuggling works on v0.38.0 (vulnerable: 0, fixed: 1)
- Server logs show: `[SMUGGLED-HIT] GET /admin` when HEAD request body contains smuggled request

### Variant 2: CL+TE Header Confusion (Secondary Issue Mentioned in Ticket)

**Entry point**: Requests with both Content-Length and Transfer-Encoding headers

**Evidence**:
- Test v2_cl_te shows v0.40.0 returns `HTTP/1.1 400 Bad Request` (fix working)
- The fix explicitly checks for and rejects these requests

### Variant 3: HTTP/1.0 Keep-Alive (Not Viable)

**Entry point**: HTTP/1.0 requests with keep-alive and Content-Length

**Result**: Not exploitable - HTTP/1.0 connections are closed by default unless explicit Connection: keep-alive is sent, and even then, modern server behavior prevents smuggling.

### Variant 4: Oversized Body (Not Viable)

**Entry point**: GET/HEAD with Content-Length larger than payload_max_length

**Result**: Not exploitable - connection is closed when drain fails due to size limit.

## Impact

**Package**: cpp-httplib (header-only C++ HTTP library)
**Tested Versions**: v0.38.0 (vulnerable), v0.40.0 (fixed), master (6607a6a)
**Risk Level**: HIGH for vulnerable versions, NONE for fixed versions

**Consequences on vulnerable versions**:
- HTTP Request Smuggling via GET or HEAD requests with Content-Length
- The HEAD variant is equally dangerous as GET since it produces the same server-side effect (body not consumed)
- Behind a reverse proxy, this can lead to cache poisoning, session hijacking, or access control bypass

## Root Cause

The HEAD variant shares the exact same root cause as the original GET vulnerability:

```cpp
// File handler
if ((req.method == "GET" || req.method == "HEAD") &&
    handle_file_request(req, res)) {
  return true;  // Early return - body not consumed
}

if (detail::expect_content(req)) {  // Never reached for static files
  // Content reader handler - would read the body
  ...
}
```

The vulnerability exists because:
1. Both GET and HEAD are handled by the same `handle_file_request()` path
2. Both cause early return from `routing()` before `expect_content()` is checked
3. Neither method semantically expects a body, but the server accepts one if Content-Length is provided
4. The unconsumed body remains in the TCP buffer for the next request on keep-alive connections

The fix addresses this by:
1. Tracking body consumption via `body_consumed_` flag set in `read_content_core()`
2. Adding post-processing drain code at the end of `process_request()`:
```cpp
// Drain any unconsumed request body to prevent request smuggling on
// keep-alive connections.
if (!req.body_consumed_ && detail::expect_content(req)) {
  int drain_status = 200;
  if (!detail::read_content(...)) {
    connection_closed = true;
  }
}
```

## Reproduction Steps

1. Run `vuln_variant/reproduction_steps.sh` which:
   - Builds test servers for v0.38.0 and v0.40.0
   - Tests 5 different variant scenarios against both versions
   - Captures server logs and response data
   - Generates a summary report

2. Key variant test (HEAD method):
   - Send: `HEAD /index.html HTTP/1.1` with `Content-Length: 37` (size of smuggled request)
   - Wait for first response (headers only for HEAD)
   - Send body: `GET /admin HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n`
   - On v0.38.0: Second response contains admin content (smuggling successful)
   - On v0.40.0: No second response (body was drained, fix working)

## Evidence

**Log locations**:
- Test execution log: `logs/vuln_variant/test_run.log`
- Per-test server logs: `logs/vuln_variant/server_{version}_{variant}.log`
- Per-test payload logs: `logs/vuln_variant/payload_{version}_{variant}.log`
- Final results: `logs/vuln_variant/final_variant_results.json`

**Key excerpts**:
```
Variant Results (0=vulnerable, 1=not vulnerable):
v1_get_baseline (original)     | 0               | 1              
v2_cl_te (CL+TE)               | 1               | 1              
v3_head (HEAD method)          | 0               | 1              
v4_oversized (large body)      | 1               | 1              
v5_http10 (HTTP/1.0)           | 1               | 1              
```

**HEAD variant confirmation on v0.38.0**:
```
[TEST] Variant: v3_head | Version: v0.38.0
First response: 186 bytes
Second response: 112 bytes
ADMIN ACCESS GRANTED
[VULNERABLE] Smuggling detected!
```

**HEAD variant blocked on v0.40.0**:
```
[TEST] Variant: v3_head | Version: v0.40.0
First response: 186 bytes
Second response: 0 bytes
[NOT VULNERABLE] No smuggling
```

## Recommendations / Next Steps

1. **The fix is complete**: The v0.40.0 fix properly addresses both GET and HEAD variants. No additional code changes are needed.

2. **Test coverage recommendation**: The existing test suite in the fix commit (`RequestSmugglingTest.UnconsumedGETBodyOnFileHandler`) only tests GET. Consider adding a HEAD variant test:
```cpp
TEST(RequestSmugglingTest, UnconsumedHEADBodyOnFileHandler) {
  // Similar to GET test but using HEAD method
  // Verifies that HEAD requests also have their bodies drained
}
```

3. **Documentation**: Consider documenting that both GET and HEAD methods are affected by the original vulnerability and both are fixed in v0.40.0.

4. **No bypass found**: After testing 5 distinct variant approaches, no bypass of the fixed version was found. The fix comprehensively addresses the vulnerability class.

## Additional Notes

- **Idempotency**: The reproduction script is idempotent and can be run multiple times without side effects
- **Environment**: Tests run on localhost (127.0.0.1) with isolated ports (29999)
- **Cleanup**: Script properly kills test servers after each test
- **Source identity**: 
  - Vulnerable: v0.38.0 (6f2717e97f5e5dbd35178c0f2d9d6c9496a0d90c)
  - Fixed: v0.40.0 (b7e02de1afab7bf93c412b1c9f026a9b3f7f79ba)
  - Latest: master (6607a6a5922b8cf78d01f3f4674970ccbfda73cd)
