# Patch Analysis - CVE-2026-34441 Fix

## Fix Commit

**Commit**: 6fd97aeca0faa1c6e1bd7ae8150c821dcff31c3b
**Message**: "Implement request body consumption and reject invalid Content-Length with Transfer-Encoding to prevent request smuggling"
**Date**: Fri Mar 27 23:16:08 2026 -0400
**Author**: yhirose

## What the Fix Changes

### 1. Added Body Consumption Tracking (3 locations)

**File**: httplib.h
**Location**: Request struct (line 1273)

```cpp
// private members...
bool body_consumed_ = false;  // NEW: Track if body was consumed
```

**Location**: `read_content_core()` (line 11333)

```cpp
if (!detail::read_content(...)) {
  return false;
}

req.body_consumed_ = true;  // NEW: Mark body as consumed

if (req.is_multipart_form_data()) {
  ...
}
```

### 2. Added Post-Processing Body Drain (1 location)

**File**: httplib.h
**Location**: `process_request()` end (lines 12182-12195)

```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; // required by read_content signature
  if (!detail::read_content(
          strm, req, payload_max_length_, drain_status, nullptr,
          [](const char *, size_t, size_t, size_t) { return true; }, false)) {
    // Body exceeds payload limit or read error — close the connection
    // to prevent leftover bytes from being misinterpreted.
    connection_closed = true;
  }
}
```

This is the **primary fix** - it ensures that even when handlers return early (like `handle_file_request()`), any unconsumed body is drained before the connection is reused.

### 3. Added CL+TE Rejection (1 location)

**File**: httplib.h
**Location**: `process_request()` header validation (lines 11951-11958)

```cpp
// RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and
// any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is
// tolerated for compatibility with existing clients.
if (req.get_header_value_u64("Content-Length") > 0 &&
    req.has_header("Transfer-Encoding")) {
  connection_closed = true;
  res.status = StatusCode::BadRequest_400;
  return write_response(strm, close_connection, req, res);
}
```

This is the **secondary fix** - it prevents CL+TE request smuggling attacks by rejecting ambiguous requests.

### 4. Added Connection Closure for Error Paths (3 locations)

The fix adds `connection_closed = true` before early returns in error paths:
- URI too long (414 error)
- Accept header parsing failure (400 error)
- Range header parsing failure (416 error)

This ensures that when errors occur, the connection is not reused, preventing any potential smuggling.

## Fix Assumptions

The fix makes the following assumptions:

1. **All request paths flow through `process_request()`**: The drain code is at the end of this function, so it catches all routing outcomes including:
   - Successful static file serving (GET/HEAD)
   - Handler errors and early returns
   - Exception handling paths (both `std::exception` and `...` catch blocks)
   - WebSocket upgrade (but this explicitly closes the connection anyway)

2. **`expect_content()` correctly identifies body expectations**: The fix relies on this function to determine if a body should be drained.

3. **Draining is safe for all stream states**: The drain code runs after response writing, which may be safe but assumes the stream is still readable.

4. **Performance cost of draining is acceptable**: For legitimate requests with bodies (POST/PUT), the body is already consumed, so no extra work. For GET/HEAD with malicious Content-Length, the body is drained, which costs bandwidth but prevents the attack.

## What the Fix Does NOT Cover

### Not Covered (By Design):

1. **Content-Length: 0 with Transfer-Encoding**: The fix comment explicitly states: "Content-Length: 0 is tolerated for compatibility with existing clients." This is a deliberate RFC compliance decision.

2. **WebSocket upgrade requests**: These set `connection_closed = true` explicitly, so they don't need the drain mechanism. However, if a WebSocket upgrade request had both CL>0 and TE, it would be rejected by the CL+TE check.

### Potential Gaps (Not Exploitable in Testing):

1. **Exception during drain**: If an exception occurs during the drain `read_content()` call, the catch block sets `connection_closed = true`, so this is handled.

2. **Partial body already read**: If some body bytes were read but not marked as consumed, they would remain. However, `body_consumed_` is only set when `read_content()` completes successfully, so partial reads don't set it.

3. **Custom content providers that don't set body_consumed_**: The `body_consumed_` flag is set in `read_content_core()`, which is used by the standard content reading flow. Custom handlers that bypass this would need to set the flag themselves.

## Behavior Comparison

### Before Fix (v0.38.0)

**Vulnerable flow for GET /index.html with body:**
1. `routing()` called
2. `handle_file_request()` returns true
3. `routing()` returns true immediately
4. Response written, connection kept alive
5. Body bytes remain in stream
6. Next request reads body bytes as new request

### After Fix (v0.40.0)

**Fixed flow for GET /index.html with body:**
1. `routing()` called
2. `handle_file_request()` returns true
3. `routing()` returns true immediately
4. Response written
5. **Drain code runs**: `!req.body_consumed_ && expect_content(req)` is true
6. **Body is read and discarded**
7. Connection kept alive (safely)
8. Next request reads actual next request from stream

## Testing Results

All tested variants are blocked by the fix:

| Variant | v0.38.0 | v0.40.0 | Bypass? |
|---------|---------|---------|---------|
| GET with Content-Length | Vulnerable | Blocked | No |
| HEAD with Content-Length | Vulnerable | Blocked | No |
| CL+TE headers | Not exploitable | Blocked (400) | N/A |
| HTTP/1.0 keep-alive | Not exploitable | Blocked | N/A |
| Oversized body | Connection closed | Connection closed | N/A |

## Assessment

The fix is **comprehensive and complete**. It:
1. Addresses the root cause (unconsumed bodies)
2. Handles the secondary issue (CL+TE confusion)
3. Covers all code paths including error handlers and exceptions
4. Correctly handles the HEAD variant (same as GET)
5. Closes connections appropriately when drain fails
6. Adds proper error path connection closure

**No bypass was found** after testing 5 distinct attack variants.
