Inkonsistente Interpretation von HTTP-Anfragen ('HTTP Request Smuggling')

Beschreibung

HTTP Request Smuggling tritt auf, wenn Front-End- und Back-End-Server HTTP-Anfragegrenzen unterschiedlich interpretieren. Diese Diskrepanz entsteht durch mehrdeutige Behandlung von Content-Length- und Transfer-Encoding-Headern oder fehlerhafte HTTP-Anfragen. Angreifer nutzen dies aus, um eine versteckte Anfrage zu "schmuggeln", die Sicherheitskontrollen umgeht, auf Sitzungen anderer Benutzer zugreift oder Web-Caches vergiftet. Die Technik nutzt die fundamentale Vertrauensbeziehung zwischen verketteten HTTP-Servern aus.

Risiko

HTTP Request Smuggling kann Web-Application-Firewalls, Zugriffskontrolllisten und Authentifizierungsmechanismen umgehen. Angreifer können Anfragen anderer Benutzer kapern, Anmeldedaten erfassen oder Caches vergiften, um bösartigen Inhalt allen Benutzern zu liefern. Die Schwachstelle hat zu kritischen Exploits auf großen Plattformen geführt. Erkennung ist schwierig, da Angriffe wie legitimer Verkehr aussehen. Moderne Infrastruktur mit Load Balancern, CDNs und Reverse Proxies schafft viele Möglichkeiten für Smuggling.

Lösung

Stellen Sie sicher, dass alle Server in der Kette HTTP-Anfragen identisch interpretieren. Konfigurieren Sie Front-End-Server so, dass mehrdeutige Anfragen normalisiert werden. Lehnen Sie Anfragen mit sowohl Content-Length als auch Transfer-Encoding-Headern ab. Verwenden Sie HTTP/2 durchgehend wo möglich (es hat explizites Längen-Framing). Deaktivieren Sie bei Bedarf Verbindungswiederverwendung. Implementieren Sie striktes HTTP-Parsing, das fehlerhafte Anfragen ablehnt. Überwachen Sie auf ungewöhnliche Anfragemuster. Verwenden Sie Web-Application-Firewalls, die Smuggling-Versuche erkennen.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleUmfang: Sicherheitsumgehung

Geschmuggelte Anfragen umgehen WAFs, Authentifizierung und Zugriffskontrollen.
VertraulichkeitUmfang: Request Hijacking

Angreifer können Anfragen anderer Benutzer einschließlich Anmeldedaten erfassen und lesen.
IntegritätUmfang: Cache Poisoning

Bösartige Antworten können gecacht und allen Benutzern ausgeliefert werden.

Beispielcode + Lösungscode

Anfällige Szenarien

# CL.TE-Angriff (Front-End verwendet Content-Length, Back-End verwendet Transfer-Encoding)
# ANFÄLLIG: Mehrdeutige Anfrage

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

0

SMUGGLED

# Front-End sieht Content-Length: 13, leitet "0\r\n\r\nSMUGGLED" weiter
# Back-End sieht Chunked-Encoding, verarbeitet "0\r\n\r\n" als Ende,
# "SMUGGLED" wird Anfang der nächsten Anfrage!
# TE.CL-Angriff (Front-End verwendet Transfer-Encoding, Back-End verwendet Content-Length)
# ANFÄLLIG: Smuggling via TE.CL

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

8
SMUGGLED
0

# Front-End verarbeitet Chunked, sendet alles
# Back-End liest nur 3 Bytes ("8\r\n"), lässt "SMUGGLED..." für nächste Anfrage
# TE.TE-Angriff (Verschleiertes Transfer-Encoding)
# ANFÄLLIG: Transfer-Encoding-Verschleierung

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

# Server sind uneinig, welches Transfer-Encoding zu verwenden ist
# ANFÄLLIG: Server behandelt mehrdeutige Header nicht
from http.server import HTTPServer, BaseHTTPRequestHandler

class VulnerableHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        # Content-Length lesen ohne Transfer-Encoding zu prüfen
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length)

        # Wenn Transfer-Encoding: chunked auch vorhanden war,
        # ist die Body-Interpretation inkonsistent
        self.process_request(body)
// ANFÄLLIG: Express mit inkonsistentem Parsing
const express = require('express');
const app = express();

// Standard-Body-Parser behandelt möglicherweise nicht alle Randfälle
app.use(express.json());

// Proxy-Weiterleitung ohne Normalisierung
app.use('/api', (req, res) => {
    // Einfaches Weiterleiten ohne Header-Bereinigung
    proxy.web(req, res, { target: 'http://backend' });
});

Korrigierter Code

# SICHER: Strikte HTTP-Header-Behandlung
from http.server import HTTPServer, BaseHTTPRequestHandler

class SecureHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        # Auf mehrdeutige Header prüfen
        content_length = self.headers.get('Content-Length')
        transfer_encoding = self.headers.get('Transfer-Encoding')

        # Anfragen mit beiden Headern ablehnen
        if content_length and transfer_encoding:
            self.send_error(400, "Mehrdeutige Anfrage: sowohl CL als auch TE vorhanden")
            return

        # Transfer-Encoding behandeln
        if transfer_encoding:
            if transfer_encoding.lower() != 'chunked':
                self.send_error(400, "Nicht unterstütztes 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, "Ungültiges Content-Length")
                    return
                body = self.rfile.read(length)
            except ValueError:
                self.send_error(400, "Ungültiges Content-Length")
                return
        else:
            body = b''

        self.process_request(body)

    def read_chunked_body(self):
        """Chunked Body sicher lesen."""
        body = b''
        while True:
            line = self.rfile.readline()
            try:
                chunk_size = int(line.strip(), 16)
            except ValueError:
                raise ValueError("Ungültige Chunk-Größe")

            if chunk_size == 0:
                break

            if chunk_size > MAX_CHUNK_SIZE:
                raise ValueError("Chunk zu groß")

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

        return body
# SICHER: Nginx-Konfiguration zur Smuggling-Prävention

http {
    # Anfragen vor Weiterleitung normalisieren

    # Anfragen mit sowohl Content-Length als auch Transfer-Encoding ablehnen
    map $http_transfer_encoding $reject_request {
        default 0;
        "~*chunked" $http_content_length;
    }

    server {
        listen 443 ssl http2;

        # HTTP/2 zum Backend wenn möglich verwenden
        location / {
            # Mehrdeutige Anfragen ablehnen
            if ($reject_request) {
                return 400;
            }

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

            # Potenziell gefährliche Header entfernen
            proxy_set_header Transfer-Encoding "";

            # Content-Length neu berechnen
            proxy_set_header Content-Length $content_length;

            proxy_pass http://backend;
        }
    }
}
// SICHER: Express mit strikter HTTP-Behandlung
const express = require('express');
const app = express();

// Middleware zur Erkennung von Smuggling-Versuchen
function detectSmuggling(req, res, next) {
    const contentLength = req.headers['content-length'];
    const transferEncoding = req.headers['transfer-encoding'];

    // Anfragen mit beiden Headern ablehnen
    if (contentLength && transferEncoding) {
        return res.status(400).send('Bad Request: Mehrdeutige Header');
    }

    // Fehlerhaftes Transfer-Encoding ablehnen
    if (transferEncoding && transferEncoding.toLowerCase() !== 'chunked') {
        return res.status(400).send('Bad Request: Ungültiges Transfer-Encoding');
    }

    // Auf Header-Smuggling via Whitespace/Null-Bytes prüfen
    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: Ungültiger Header');
        }
    }

    next();
}

app.use(detectSmuggling);

// Header vor Proxying normalisieren
const { createProxyMiddleware } = require('http-proxy-middleware');

app.use('/api', createProxyMiddleware({
    target: 'http://backend:8080',
    changeOrigin: true,
    onProxyReq: (proxyReq, req, res) => {
        // Potenziell gefährliche Header entfernen
        proxyReq.removeHeader('transfer-encoding');

        // Sicherstellen, dass Content-Length korrekt ist
        if (req.body) {
            const bodyData = JSON.stringify(req.body);
            proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
        }
    }
}));
// SICHER: Java-Servlet-Filter zur Smuggling-Prävention
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");

        // Mehrdeutige Anfragen ablehnen
        if (contentLength != null && transferEncoding != null) {
            httpResponse.sendError(400, "Mehrdeutige Anfrage-Header");
            return;
        }

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

        // Transfer-Encoding validieren
        if (transferEncoding != null) {
            if (!transferEncoding.equalsIgnoreCase("chunked")) {
                httpResponse.sendError(400, "Nicht unterstütztes Transfer-Encoding");
                return;
            }
        }

        // Auf doppelte Header prüfen (könnte Smuggling anzeigen)
        if (hasDuplicateHeader(httpRequest, "Content-Length") ||
            hasDuplicateHeader(httpRequest, "Transfer-Encoding")) {
            httpResponse.sendError(400, "Doppelte kritische Header");
            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;
    }
}
// SICHER: Go Reverse Proxy mit Smuggling-Prävention
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) {
        // Auf sowohl Content-Length als auch Transfer-Encoding prüfen
        hasContentLength := r.Header.Get("Content-Length") != ""
        hasTransferEncoding := r.Header.Get("Transfer-Encoding") != ""

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

        // Auf verschleiertes Transfer-Encoding prüfen
        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
            }
        }

        // Auf Header-Injection prüfen
        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)

    // Proxy-Director modifizieren um Anfragen zu normalisieren
    originalDirector := proxy.Director
    proxy.Director = func(r *http.Request) {
        originalDirector(r)

        // Mehrdeutige Header entfernen
        r.Header.Del("Transfer-Encoding")

        // HTTP/1.1 mit Connection: close verwenden um Smuggling zu verhindern
        r.Header.Set("Connection", "close")
    }

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

Ausgenutzt in der Praxis

Capital One WAF-Umgehung (2019)

HTTP Request Smuggling wurde verwendet, um AWS WAF-Schutzmaßnahmen zu umgehen, was zu einer großen Datenpanne beitrug, die über 100 Millionen Kunden betraf.

Mehrere CDN-Schwachstellen (2019-2020)

Forscher demonstrierten HTTP Request Smuggling-Angriffe gegen große CDNs einschließlich Akamai, Cloudflare und ändere, die Cache Poisoning und Request Hijacking ermöglichten.

Slack Bug Bounty (2019)

Eine kritische HTTP Request Smuggling-Schwachstelle in Slacks Infrastruktur erlaubte Angreifern, Anfragen anderer Benutzer zu kapern.


Tools zum Testen/Ausnutzen


CVE-Beispiele


Referenzen

  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