Inconsistent Interpretation of HTTP Requests ('HTTP Request Smuggling')

Description

HTTP Request Smuggling occurs when front-end and back-end servers interpret HTTP request boundaries differently. This discrepancy arises from ambiguous handling of Content-Length and Transfer-Encoding headers, or malformed HTTP requests. Attackers exploit this to "smuggle" a hidden request that bypasses security controls, accesses other users' sessions, or poisons web caches. The technique exploits the fundamental trust relationship between chained HTTP servers.

Risk

HTTP Request Smuggling can bypass web application firewalls, access control lists, and authentication mechanisms. Attackers can hijack other users' requests, capture credentials, or poison caches to serve malicious content to all users. The vulnerability has led to critical exploits in major platforms. Detection is difficult because attacks look like legitimate traffic. Modern infrastructure with load balancers, CDNs, and reverse proxies creates many opportunities for smuggling.

Solution

Ensure all servers in the chain interpret HTTP requests identically. Configure front-end servers to normalize ambiguous requests. Reject requests with both Content-Length and Transfer-Encoding headers. Use HTTP/2 end-to-end where possible (it has explicit length framing). Disable connection reuse if necessary. Implement strict HTTP parsing that rejects malformed requests. Monitor for unusual request patterns. Use web application firewalls that detect smuggling attempts.

Common Consequences

ImpactDetails
Access ControlScope: Security Bypass

Smuggled requests bypass WAFs, authentication, and access controls.
ConfidentialityScope: Request Hijacking

Attackers can capture and read other users' requests including credentials.
IntegrityScope: Cache Poisoning

Malicious responses can be cached and served to all users.

Example Code + Solution Code

Vulnerable Scenarios

# CL.TE Attack (Front-end uses Content-Length, Back-end uses Transfer-Encoding)
# VULNERABLE: Ambiguous request

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED

# Front-end sees Content-Length: 13, forwards "0\r\n\r\nSMUGGLED"
# Back-end sees chunked encoding, processes "0\r\n\r\n" as end,
# "SMUGGLED" becomes start of next request!
# TE.CL Attack (Front-end uses Transfer-Encoding, Back-end uses Content-Length)
# VULNERABLE: Smuggling via TE.CL

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 3
Transfer-Encoding: chunked

8
SMUGGLED
0

# Front-end processes chunked, sends everything
# Back-end reads only 3 bytes ("8\r\n"), leaves "SMUGGLED..." for next request
# TE.TE Attack (Obfuscated Transfer-Encoding)
# VULNERABLE: Transfer-Encoding obfuscation

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 4
Transfer-Encoding: chunked
Transfer-Encoding: x

5c
GPOST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
0

# Servers disagree on which Transfer-Encoding to use
# VULNERABLE: Server not handling ambiguous headers
from http.server import HTTPServer, BaseHTTPRequestHandler

class VulnerableHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        # Reading Content-Length without checking Transfer-Encoding
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length)

        # If Transfer-Encoding: chunked was also present,
        # the body interpretation is inconsistent
        self.process_request(body)
// VULNERABLE: Express with inconsistent parsing
const express = require('express');
const app = express();

// Default body parser might not handle all edge cases
app.use(express.json());

// Proxy forwarding without normalization
app.use('/api', (req, res) => {
    // Simply forwarding without sanitizing headers
    proxy.web(req, res, { target: 'http://backend' });
});

Fixed Code

# SAFE: Strict HTTP header handling
from http.server import HTTPServer, BaseHTTPRequestHandler

class SecureHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        # Check for ambiguous headers
        content_length = self.headers.get('Content-Length')
        transfer_encoding = self.headers.get('Transfer-Encoding')

        # Reject requests with both headers
        if content_length and transfer_encoding:
            self.send_error(400, "Ambiguous request: both CL and TE present")
            return

        # Handle Transfer-Encoding
        if transfer_encoding:
            if transfer_encoding.lower() != 'chunked':
                self.send_error(400, "Unsupported Transfer-Encoding")
                return
            body = self.read_chunked_body()
        elif content_length:
            try:
                length = int(content_length)
                if length < 0 or length > MAX_BODY_SIZE:
                    self.send_error(400, "Invalid Content-Length")
                    return
                body = self.rfile.read(length)
            except ValueError:
                self.send_error(400, "Invalid Content-Length")
                return
        else:
            body = b''

        self.process_request(body)

    def read_chunked_body(self):
        """Safely read chunked body."""
        body = b''
        while True:
            line = self.rfile.readline()
            try:
                chunk_size = int(line.strip(), 16)
            except ValueError:
                raise ValueError("Invalid chunk size")

            if chunk_size == 0:
                break

            if chunk_size > MAX_CHUNK_SIZE:
                raise ValueError("Chunk too large")

            body += self.rfile.read(chunk_size)
            self.rfile.read(2)  # CRLF

        return body
# SAFE: Nginx configuration to prevent smuggling

http {
    # Normalize requests before forwarding

    # Reject requests with both Content-Length and Transfer-Encoding
    map $http_transfer_encoding $reject_request {
        default 0;
        "~*chunked" $http_content_length;
    }

    server {
        listen 443 ssl http2;

        # Use HTTP/2 to backend if possible
        location / {
            # Reject ambiguous requests
            if ($reject_request) {
                return 400;
            }

            # Normalize Transfer-Encoding
            proxy_http_version 1.1;
            proxy_set_header Connection "";

            # Remove potentially dangerous headers
            proxy_set_header Transfer-Encoding "";

            # Recalculate Content-Length
            proxy_set_header Content-Length $content_length;

            proxy_pass http://backend;
        }
    }
}
// SAFE: Express with strict HTTP handling
const express = require('express');
const app = express();

// Middleware to detect smuggling attempts
function detectSmuggling(req, res, next) {
    const contentLength = req.headers['content-length'];
    const transferEncoding = req.headers['transfer-encoding'];

    // Reject requests with both headers
    if (contentLength && transferEncoding) {
        return res.status(400).send('Bad Request: Ambiguous headers');
    }

    // Reject malformed Transfer-Encoding
    if (transferEncoding && transferEncoding.toLowerCase() !== 'chunked') {
        return res.status(400).send('Bad Request: Invalid Transfer-Encoding');
    }

    // Check for header smuggling via whitespace/null bytes
    for (const [key, value] of Object.entries(req.headers)) {
        if (/[\r\n\0]/.test(key) || /[\r\n\0]/.test(value)) {
            return res.status(400).send('Bad Request: Invalid header');
        }
    }

    next();
}

app.use(detectSmuggling);

// Normalize headers before proxying
const { createProxyMiddleware } = require('http-proxy-middleware');

app.use('/api', createProxyMiddleware({
    target: 'http://backend:8080',
    changeOrigin: true,
    onProxyReq: (proxyReq, req, res) => {
        // Remove potentially dangerous headers
        proxyReq.removeHeader('transfer-encoding');

        // Ensure Content-Length is accurate
        if (req.body) {
            const bodyData = JSON.stringify(req.body);
            proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
        }
    }
}));
// SAFE: Java servlet filter for smuggling prevention
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;

public class AntiSmugglingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String contentLength = httpRequest.getHeader("Content-Length");
        String transferEncoding = httpRequest.getHeader("Transfer-Encoding");

        // Reject ambiguous requests
        if (contentLength != null && transferEncoding != null) {
            httpResponse.sendError(400, "Ambiguous request headers");
            return;
        }

        // Validate Content-Length
        if (contentLength != null) {
            try {
                long length = Long.parseLong(contentLength);
                if (length < 0) {
                    httpResponse.sendError(400, "Invalid Content-Length");
                    return;
                }
            } catch (NumberFormatException e) {
                httpResponse.sendError(400, "Invalid Content-Length");
                return;
            }
        }

        // Validate Transfer-Encoding
        if (transferEncoding != null) {
            if (!transferEncoding.equalsIgnoreCase("chunked")) {
                httpResponse.sendError(400, "Unsupported Transfer-Encoding");
                return;
            }
        }

        // Check for duplicate headers (could indicate smuggling)
        if (hasDuplicateHeader(httpRequest, "Content-Length") ||
            hasDuplicateHeader(httpRequest, "Transfer-Encoding")) {
            httpResponse.sendError(400, "Duplicate critical headers");
            return;
        }

        chain.doFilter(request, response);
    }

    private boolean hasDuplicateHeader(HttpServletRequest request, String headerName) {
        java.util.Enumeration<String> values = request.getHeaders(headerName);
        int count = 0;
        while (values.hasMoreElements()) {
            values.nextElement();
            count++;
            if (count > 1) return true;
        }
        return false;
    }
}
// SAFE: Go reverse proxy with smuggling prevention
package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
    "strings"
)

func antiSmugglingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Check for both Content-Length and Transfer-Encoding
        hasContentLength := r.Header.Get("Content-Length") != ""
        hasTransferEncoding := r.Header.Get("Transfer-Encoding") != ""

        if hasContentLength && hasTransferEncoding {
            http.Error(w, "Bad Request", http.StatusBadRequest)
            return
        }

        // Check for obfuscated Transfer-Encoding
        for _, te := range r.Header.Values("Transfer-Encoding") {
            normalized := strings.ToLower(strings.TrimSpace(te))
            if normalized != "chunked" && normalized != "" {
                http.Error(w, "Bad Request", http.StatusBadRequest)
                return
            }
        }

        // Check for header injection
        for key, values := range r.Header {
            if strings.ContainsAny(key, "\r\n\x00") {
                http.Error(w, "Bad Request", http.StatusBadRequest)
                return
            }
            for _, v := range values {
                if strings.ContainsAny(v, "\r\n\x00") {
                    http.Error(w, "Bad Request", http.StatusBadRequest)
                    return
                }
            }
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    backend, _ := url.Parse("http://backend:8080")
    proxy := httputil.NewSingleHostReverseProxy(backend)

    // Modify proxy director to normalize requests
    originalDirector := proxy.Director
    proxy.Director = func(r *http.Request) {
        originalDirector(r)

        // Remove ambiguous headers
        r.Header.Del("Transfer-Encoding")

        // Use HTTP/1.1 with Connection: close to prevent smuggling
        r.Header.Set("Connection", "close")
    }

    http.Handle("/", antiSmugglingMiddleware(proxy))
    http.ListenAndServe(":8080", nil)
}

Exploited in the Wild

Capital One WAF Bypass (2019)

HTTP request smuggling was used to bypass AWS WAF protections, contributing to a major data breach affecting over 100 million customers.

Multiple CDN Vulnerabilities (2019-2020)

Researchers demonstrated HTTP request smuggling attacks against major CDNs including Akamai, Cloudflare, and others, enabling cache poisoning and request hijacking.

Slack Bug Bounty (2019)

A critical HTTP request smuggling vulnerability in Slack's infrastructure allowed attackers to hijack other users' requests.


Tools to test/exploit


CVE Examples


References

  1. MITRE. "CWE-444: Inconsistent Interpretation of HTTP Requests." https://cwe.mitre.org/data/definitions/444.html

  2. PortSwigger. "HTTP Request Smuggling." https://portswigger.net/web-security/request-smuggling