Unsachgemäße Neutralisierung von HTTP-Headern für Scripting-Syntax

Beschreibung

Unsachgemäße Neutralisierung von HTTP-Headern für Scripting-Syntax tritt auf, wenn benutzergesteuerte Daten in HTTP-Response-Header eingefügt werden, ohne ordnungsgemäße Sanitisierung. Angreifer können Newline-Zeichen (CRLF - \r\n) injizieren, um beliebige Header hinzuzufügen oder bestehende zu modifizieren, was potenziell zu HTTP Response Splitting, Cache Poisoning, Cross-Site Scripting über Header oder Session-Fixation-Angriffen führt.

Risiko

HTTP Response Splitting ermöglicht die Injection vollständig neuer HTTP-Responses. Cache Poisoning betrifft alle Benutzer, die gecachten bösartigen Inhalt erhalten. XSS durch Header wie Content-Type oder Content-Disposition. Session-Fixation durch Injizieren von Set-Cookie-Headern. Header-Injection kann Benutzer auf bösartige Seiten umleiten. Sicherheitsheader (CSP, X-XSS-Protection) können überschrieben oder entfernt werden.

Lösung

Validieren und sanitisieren Sie alle Benutzereingaben, bevor Sie sie in Header einbinden. Lehnen Sie CR (\r, %0d) und LF (\n, %0a) Zeichen ab oder kodieren Sie sie. Verwenden Sie Framework-bereitgestellte Header-Setzfunktionen, die automatisch escapen. Implementieren Sie strikte Eingabevalidierung mit Whitelisting. Verifizieren Sie, dass Header-Werte erwarteten Formaten entsprechen. Verwenden Sie Sicherheitsframeworks, die Header-Injection verhindern.

Häufige Auswirkungen

AuswirkungDetails
IntegritätBereich: Response-Manipulation

HTTP-Responses modifiziert oder gesplittet.
VertraulichkeitBereich: Datenexposition

Cache Poisoning exponiert Daten an falsche Benutzer.
ZugriffskontrolleBereich: Session-Angriffe

Session-Fixation oder -Hijacking.

Beispielcode + Lösungscode

Verwundbarer Code

// VERWUNDBAR: Benutzereingabe direkt in Header
@WebServlet("/redirect")
public class VulnerableRedirectServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String target = req.getParameter("url");

        // Verwundbar: CRLF-Injection möglich
        resp.setHeader("Location", target);
        resp.setStatus(302);

        // Angriff: url=http://good.com%0d%0aSet-Cookie:%20admin=true
        // Ergibt:
        // Location: http://good.com
        // Set-Cookie: admin=true
    }
}

// VERWUNDBAR: Dateiname in Content-Disposition
@WebServlet("/download")
public class VulnerableDownloadServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String filename = req.getParameter("file");

        // Verwundbar: CRLF-Injection in Header
        resp.setHeader("Content-Disposition",
                       "attachment; filename=" + filename);

        // Angriff: file=test.txt%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(1)</script>
        // Verursacht XSS!
    }
}

// VERWUNDBAR: Benutzerdefinierter Header aus Benutzereingabe
@WebServlet("/api")
public class VulnerableApiServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String requestId = req.getParameter("requestId");

        // Verwundbar
        resp.setHeader("X-Request-ID", requestId);

        // Angriff: requestId=abc%0d%0aSet-Cookie:%20session=hijacked
    }
}
# VERWUNDBAR: Flask Header-Injection
from flask import Flask, request, make_response, redirect

app = Flask(__name__)

@app.route('/redirect')
def redirect_vulnerable():
    url = request.args.get('url')
    # Verwundbar: CRLF-Injection
    response = redirect(url)
    return response
    # Angriff: url=http://good.com%0d%0aX-Injected:%20value

@app.route('/download')
def download_vulnerable():
    filename = request.args.get('filename')
    response = make_response(get_file_content())
    # Verwundbarer Header
    response.headers['Content-Disposition'] = f'attachment; filename={filename}'
    return response
    # Angriff: filename=test%0d%0aContent-Type:%20text/html

@app.route('/api/data')
def api_vulnerable():
    callback = request.args.get('callback')
    response = make_response(get_data())
    # Verwundbar: Benutzereingabe in Header
    response.headers['X-Callback'] = callback
    return response
    # Angriff: callback=test%0d%0aSet-Cookie:%20admin=true

# VERWUNDBAR: Response Splitting für XSS
@app.route('/search')
def search_vulnerable():
    query = request.args.get('q')
    response = make_response(f"Suchergebnisse für: {query}")
    # Verwundbarer Header
    response.headers['X-Search-Query'] = query
    return response
    # Angriff: q=test%0d%0a%0d%0a<html><script>alert(document.cookie)</script>
// VERWUNDBAR: Node.js/Express Header-Injection
const express = require('express');
const app = express();

app.get('/redirect', (req, res) => {
    const url = req.query.url;
    // Verwundbar: CRLF in Redirect
    res.redirect(url);
    // Angriff: url=http://good.com%0d%0aSet-Cookie:%20evil=true
});

app.get('/download', (req, res) => {
    const filename = req.query.filename;
    // Verwundbar: Benutzereingabe in Header
    res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
    res.send(fileContent);
    // Angriff: filename=test.txt%0d%0aContent-Type:%20text/html
});

app.get('/api/callback', (req, res) => {
    const callback = req.query.callback;
    // Verwundbar
    res.setHeader('X-Callback', callback);
    res.json({ data: 'response' });
    // Angriff: callback=test%0d%0aAccess-Control-Allow-Origin:%20*
});

// VERWUNDBAR: Sprachpräferenz-Header
app.get('/content', (req, res) => {
    const lang = req.query.lang;
    // Verwundbar
    res.setHeader('Content-Language', lang);
    res.send(getContent(lang));
    // Angriff: lang=en%0d%0aSet-Cookie:%20session=stolen
});
<?php
// VERWUNDBAR: Direktes Header-Setzen
$redirect = $_GET['redirect'];
// Verwundbar: CRLF-Injection
header("Location: $redirect");
// Angriff: redirect=http://good.com%0d%0aSet-Cookie:%20admin=true

// VERWUNDBAR: Cookie aus Benutzereingabe
$theme = $_GET['theme'];
// Verwundbar
header("Set-Cookie: theme=$theme");
// Angriff: theme=dark%0d%0aSet-Cookie:%20session=hijacked

// VERWUNDBAR: Content-Disposition
$filename = $_GET['file'];
// Verwundbar
header("Content-Disposition: attachment; filename=$filename");
// Angriff: file=test.pdf%0d%0aContent-Type:%20text/html

// VERWUNDBAR: Benutzerdefinierte Header
$requestId = $_GET['id'];
// Verwundbar
header("X-Request-ID: $requestId");
// Angriff: id=123%0d%0a%0d%0a<script>alert(1)</script>
// HTTP Response Splitting - komplette neue Response injiziert
?>

Lösungscode

// SICHER: Validierte Header-Werte
@WebServlet("/redirect")
public class SafeRedirectServlet extends HttpServlet {

    private static final Set<String> ALLOWED_HOSTS = Set.of(
        "example.com", "subdomain.example.com"
    );

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String target = req.getParameter("url");

        // Validieren und sanitisieren
        if (!isValidRedirectUrl(target)) {
            resp.sendError(400, "Ungültige Redirect-URL");
            return;
        }

        // sendRedirect verwenden, das Kodierung behandelt
        resp.sendRedirect(target);
    }

    private boolean isValidRedirectUrl(String url) {
        if (url == null || url.isEmpty()) {
            return false;
        }

        // CRLF-Zeichen ablehnen
        if (url.contains("\r") || url.contains("\n") ||
            url.contains("%0d") || url.contains("%0a") ||
            url.contains("%0D") || url.contains("%0A")) {
            return false;
        }

        try {
            URI uri = new URI(url);
            // Erlaubte Hosts whitelisten
            return ALLOWED_HOSTS.contains(uri.getHost());
        } catch (URISyntaxException e) {
            return false;
        }
    }
}

// SICHER: Sanitisierte Content-Disposition
@WebServlet("/download")
public class SafeDownloadServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String filename = req.getParameter("file");

        // Dateinamen sanitisieren
        String safeFilename = sanitizeFilename(filename);
        if (safeFilename == null) {
            resp.sendError(400, "Ungültiger Dateiname");
            return;
        }

        // Ordnungsgemäße Kodierung für Content-Disposition verwenden
        String encodedFilename = URLEncoder.encode(safeFilename, "UTF-8")
                                          .replaceAll("\\+", "%20");

        resp.setHeader("Content-Disposition",
                       "attachment; filename*=UTF-8''" + encodedFilename);

        // Datei senden...
    }

    private String sanitizeFilename(String filename) {
        if (filename == null || filename.isEmpty()) {
            return null;
        }

        // Pfadkomponenten entfernen
        filename = new File(filename).getName();

        // Steuerzeichen und CRLF entfernen
        filename = filename.replaceAll("[\\x00-\\x1f\\x7f]", "");

        // Verbleibende Zeichen validieren
        if (!filename.matches("^[a-zA-Z0-9._-]+$")) {
            return null;
        }

        return filename;
    }
}

// SICHER: Response-Wrapper verwenden, der Header-Injection blockiert
public class HeaderSanitizingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletResponseWrapper wrapper = new SafeHeaderResponseWrapper(response);
        chain.doFilter(req, wrapper);
    }
}

class SafeHeaderResponseWrapper extends HttpServletResponseWrapper {

    public SafeHeaderResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public void setHeader(String name, String value) {
        super.setHeader(sanitize(name), sanitize(value));
    }

    @Override
    public void addHeader(String name, String value) {
        super.addHeader(sanitize(name), sanitize(value));
    }

    private String sanitize(String value) {
        if (value == null) return null;
        // CRLF und andere Steuerzeichen entfernen
        return value.replaceAll("[\\r\\n]", "");
    }
}
# SICHER: Flask mit ordnungsgemäßer Header-Behandlung
from flask import Flask, request, make_response, redirect, abort
from urllib.parse import urlparse, quote
import re

app = Flask(__name__)

ALLOWED_HOSTS = {'example.com', 'subdomain.example.com'}

def sanitize_header_value(value):
    """CRLF und Steuerzeichen aus Header-Wert entfernen."""
    if value is None:
        return None
    # CR, LF und andere Steuerzeichen entfernen
    return re.sub(r'[\x00-\x1f\x7f]', '', value)

def validate_redirect_url(url):
    """Redirect-URL gegen Whitelist validieren."""
    if not url:
        return None

    # Auf CRLF-Zeichen prüfen
    if any(c in url.lower() for c in ['\r', '\n', '%0d', '%0a']):
        return None

    try:
        parsed = urlparse(url)
        if parsed.netloc not in ALLOWED_HOSTS:
            return None
        return url
    except Exception:
        return None

@app.route('/redirect')
def redirect_safe():
    url = request.args.get('url')
    safe_url = validate_redirect_url(url)

    if not safe_url:
        abort(400, 'Ungültige Redirect-URL')

    return redirect(safe_url)

@app.route('/download')
def download_safe():
    filename = request.args.get('filename', '')

    # Dateinamen sanitisieren
    safe_filename = sanitize_filename(filename)
    if not safe_filename:
        abort(400, 'Ungültiger Dateiname')

    response = make_response(get_file_content())

    # RFC 5987 Kodierung für Nicht-ASCII-Dateinamen verwenden
    encoded_filename = quote(safe_filename)
    response.headers['Content-Disposition'] = \
        f"attachment; filename*=UTF-8''{encoded_filename}"

    return response

def sanitize_filename(filename):
    if not filename:
        return None

    # Pfadkomponenten entfernen
    import os
    filename = os.path.basename(filename)

    # Steuerzeichen entfernen
    filename = re.sub(r'[\x00-\x1f\x7f]', '', filename)

    # Validieren
    if not re.match(r'^[a-zA-Z0-9._-]+

```javascript
// SICHER: Express mit Header-Sanitisierung
const express = require('express');
const app = express();

const ALLOWED_HOSTS = new Set(['example.com', 'subdomain.example.com']);

function sanitizeHeaderValue(value) {
    if (!value) return '';
    // CR, LF und Steuerzeichen entfernen
    return value.replace(/[\x00-\x1f\x7f]/g, '');
}

function validateRedirectUrl(url) {
    if (!url) return null;

    // Auf CRLF prüfen
    if (/[\r\n]|%0[dD]|%0[aA]/i.test(url)) {
        return null;
    }

    try {
        const parsed = new URL(url);
        if (!ALLOWED_HOSTS.has(parsed.hostname)) {
            return null;
        }
        return url;
    } catch {
        return null;
    }
}

app.get('/redirect', (req, res) => {
    const url = req.query.url;
    const safeUrl = validateRedirectUrl(url);

    if (!safeUrl) {
        return res.status(400).send('Ungültige Redirect-URL');
    }

    res.redirect(safeUrl);
});

app.get('/download', (req, res) => {
    const filename = req.query.filename || '';

    // Dateinamen sanitisieren
    const safeFilename = sanitizeFilename(filename);
    if (!safeFilename) {
        return res.status(400).send('Ungültiger Dateiname');
    }

    // Ordnungsgemäße Kodierung verwenden
    const encodedFilename = encodeURIComponent(safeFilename);
    res.setHeader('Content-Disposition',
        `attachment; filename*=UTF-8''${encodedFilename}`);

    res.send(fileContent);
});

function sanitizeFilename(filename) {
    if (!filename) return null;

    // Pfad entfernen
    const path = require('path');
    filename = path.basename(filename);

    // Steuerzeichen entfernen
    filename = filename.replace(/[\x00-\x1f\x7f]/g, '');

    // Validieren
    if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
        return null;
    }

    return filename;
}

// Middleware zum Sanitisieren aller Header
app.use((req, res, next) => {
    const originalSetHeader = res.setHeader.bind(res);

    res.setHeader = (name, value) => {
        if (typeof value === 'string') {
            value = sanitizeHeaderValue(value);
        }
        return originalSetHeader(name, value);
    };

    next();
});

Ausgenutzt in der Praxis

HTTP Response Splitting

Cache Poisoning durch Header-Injection.

Session-Fixation

Injizieren von Set-Cookie-Headern für Session-Angriffe.

XSS über Header

Cross-Site Scripting durch manipulierte Header.


Tools zum Testen/Ausnutzen

  • Burp Suite — CRLF-Injection-Tests.
  • OWASP ZAP HTTP-Header-Fuzzing.
  • Manuelles Testen mit %0d%0a-Sequenzen.

CVE-Beispiele

  • CVE-2019-14234: Django Header-Injection.
  • Zahlreiche CRLF-Injection-CVEs in Web-Frameworks.

Referenzen

  1. MITRE. "CWE-644: Improper Neutralization of HTTP Headers for Scripting Syntax." https://cwe.mitre.org/data/definitions/644.html
  2. OWASP. "HTTP Response Splitting."