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
| Auswirkung | Details |
|---|---|
| Integrität | Bereich: Response-Manipulation HTTP-Responses modifiziert oder gesplittet. |
| Vertraulichkeit | Bereich: Datenexposition Cache Poisoning exponiert Daten an falsche Benutzer. |
| Zugriffskontrolle | Bereich: 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
- MITRE. "CWE-644: Improper Neutralization of HTTP Headers for Scripting Syntax." https://cwe.mitre.org/data/definitions/644.html
- OWASP. "HTTP Response Splitting."