Improper Neutralization of HTTP Headers for Scripting Syntax
Description
Improper Neutralization of HTTP Headers for Scripting Syntax occurs when user-controlled data is placed into HTTP response headers without proper sanitization. Attackers can inject newline characters (CRLF - \r\n) to add arbitrary headers or modify existing ones, potentially leading to HTTP Response Splitting, cache poisoning, cross-site scripting via headers, or session fixation attacks.
Risk
HTTP Response Splitting allows injection of entirely new HTTP responses. Cache poisoning affects all users receiving cached malicious content. XSS through headers like Content-Type or Content-Disposition. Session fixation by injecting Set-Cookie headers. Header injection can redirect users to malicious sites. Security headers (CSP, X-XSS-Protection) can be overwritten or removed.
Solution
Validate and sanitize all user input before including in headers. Reject or encode CR (\r, %0d) and LF (\n, %0a) characters. Use framework-provided header setting functions that auto-escape. Implement strict input validation with whitelisting. Verify header values match expected formats. Use security frameworks that prevent header injection.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Response Manipulation HTTP responses modified or split. |
| Confidentiality | Scope: Data Exposure Cache poisoning exposes data to wrong users. |
| Access Control | Scope: Session Attacks Session fixation or hijacking. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: User input directly in header
@WebServlet("/redirect")
public class VulnerableRedirectServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String target = req.getParameter("url");
// Vulnerable: CRLF injection possible
resp.setHeader("Location", target);
resp.setStatus(302);
// Attack: url=http://good.com%0d%0aSet-Cookie:%20admin=true
// Results in:
// Location: http://good.com
// Set-Cookie: admin=true
}
}
// VULNERABLE: Filename in Content-Disposition
@WebServlet("/download")
public class VulnerableDownloadServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String filename = req.getParameter("file");
// Vulnerable: CRLF injection in header
resp.setHeader("Content-Disposition",
"attachment; filename=" + filename);
// Attack: file=test.txt%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(1)</script>
// Causes XSS!
}
}
// VULNERABLE: Custom header from user input
@WebServlet("/api")
public class VulnerableApiServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String requestId = req.getParameter("requestId");
// Vulnerable
resp.setHeader("X-Request-ID", requestId);
// Attack: requestId=abc%0d%0aSet-Cookie:%20session=hijacked
}
}
# VULNERABLE: 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')
# Vulnerable: CRLF injection
response = redirect(url)
return response
# Attack: 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())
# Vulnerable header
response.headers['Content-Disposition'] = f'attachment; filename={filename}'
return response
# Attack: filename=test%0d%0aContent-Type:%20text/html
@app.route('/api/data')
def api_vulnerable():
callback = request.args.get('callback')
response = make_response(get_data())
# Vulnerable: user input in header
response.headers['X-Callback'] = callback
return response
# Attack: callback=test%0d%0aSet-Cookie:%20admin=true
# VULNERABLE: Response splitting for XSS
@app.route('/search')
def search_vulnerable():
query = request.args.get('q')
response = make_response(f"Search results for: {query}")
# Vulnerable header
response.headers['X-Search-Query'] = query
return response
# Attack: q=test%0d%0a%0d%0a<html><script>alert(document.cookie)</script>
// VULNERABLE: Node.js/Express header injection
const express = require('express');
const app = express();
app.get('/redirect', (req, res) => {
const url = req.query.url;
// Vulnerable: CRLF in redirect
res.redirect(url);
// Attack: url=http://good.com%0d%0aSet-Cookie:%20evil=true
});
app.get('/download', (req, res) => {
const filename = req.query.filename;
// Vulnerable: user input in header
res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
res.send(fileContent);
// Attack: filename=test.txt%0d%0aContent-Type:%20text/html
});
app.get('/api/callback', (req, res) => {
const callback = req.query.callback;
// Vulnerable
res.setHeader('X-Callback', callback);
res.json({ data: 'response' });
// Attack: callback=test%0d%0aAccess-Control-Allow-Origin:%20*
});
// VULNERABLE: Language preference header
app.get('/content', (req, res) => {
const lang = req.query.lang;
// Vulnerable
res.setHeader('Content-Language', lang);
res.send(getContent(lang));
// Attack: lang=en%0d%0aSet-Cookie:%20session=stolen
});
<?php
// VULNERABLE: Direct header setting
$redirect = $_GET['redirect'];
// Vulnerable: CRLF injection
header("Location: $redirect");
// Attack: redirect=http://good.com%0d%0aSet-Cookie:%20admin=true
// VULNERABLE: Cookie from user input
$theme = $_GET['theme'];
// Vulnerable
header("Set-Cookie: theme=$theme");
// Attack: theme=dark%0d%0aSet-Cookie:%20session=hijacked
// VULNERABLE: Content-Disposition
$filename = $_GET['file'];
// Vulnerable
header("Content-Disposition: attachment; filename=$filename");
// Attack: file=test.pdf%0d%0aContent-Type:%20text/html
// VULNERABLE: Custom headers
$requestId = $_GET['id'];
// Vulnerable
header("X-Request-ID: $requestId");
// Attack: id=123%0d%0a%0d%0a<script>alert(1)</script>
// HTTP Response Splitting - entire new response injected
?>
Fixed Code
// SAFE: Validated header values
@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");
// Validate and sanitize
if (!isValidRedirectUrl(target)) {
resp.sendError(400, "Invalid redirect URL");
return;
}
// Use sendRedirect which handles encoding
resp.sendRedirect(target);
}
private boolean isValidRedirectUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
// Reject CRLF characters
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);
// Whitelist allowed hosts
return ALLOWED_HOSTS.contains(uri.getHost());
} catch (URISyntaxException e) {
return false;
}
}
}
// SAFE: Sanitized Content-Disposition
@WebServlet("/download")
public class SafeDownloadServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String filename = req.getParameter("file");
// Sanitize filename
String safeFilename = sanitizeFilename(filename);
if (safeFilename == null) {
resp.sendError(400, "Invalid filename");
return;
}
// Use proper encoding for Content-Disposition
String encodedFilename = URLEncoder.encode(safeFilename, "UTF-8")
.replaceAll("\\+", "%20");
resp.setHeader("Content-Disposition",
"attachment; filename*=UTF-8''" + encodedFilename);
// Send file...
}
private String sanitizeFilename(String filename) {
if (filename == null || filename.isEmpty()) {
return null;
}
// Remove path components
filename = new File(filename).getName();
// Remove control characters and CRLF
filename = filename.replaceAll("[\\x00-\\x1f\\x7f]", "");
// Validate remaining characters
if (!filename.matches("^[a-zA-Z0-9._-]+$")) {
return null;
}
return filename;
}
}
// SAFE: Using response wrapper that blocks header injection
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;
// Remove CRLF and other control characters
return value.replaceAll("[\\r\\n]", "");
}
}
# SAFE: Flask with proper header handling
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):
"""Remove CRLF and control characters from header value."""
if value is None:
return None
# Remove CR, LF, and other control characters
return re.sub(r'[\x00-\x1f\x7f]', '', value)
def validate_redirect_url(url):
"""Validate redirect URL against whitelist."""
if not url:
return None
# Check for CRLF characters
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, 'Invalid redirect URL')
return redirect(safe_url)
@app.route('/download')
def download_safe():
filename = request.args.get('filename', '')
# Sanitize filename
safe_filename = sanitize_filename(filename)
if not safe_filename:
abort(400, 'Invalid filename')
response = make_response(get_file_content())
# Use RFC 5987 encoding for non-ASCII filenames
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
# Remove path components
import os
filename = os.path.basename(filename)
# Remove control characters
filename = re.sub(r'[\x00-\x1f\x7f]', '', filename)
# Validate
if not re.match(r'^[a-zA-Z0-9._-]+$', filename):
return None
return filename
@app.route('/api/data')
def api_safe():
callback = request.args.get('callback', '')
# Validate callback (alphanumeric only)
if not re.match(r'^[a-zA-Z0-9_]+$', callback):
callback = ''
response = make_response(get_data())
# Sanitize before setting header
safe_callback = sanitize_header_value(callback)
if safe_callback:
response.headers['X-Callback'] = safe_callback
return response
// SAFE: Express with header sanitization
const express = require('express');
const app = express();
const ALLOWED_HOSTS = new Set(['example.com', 'subdomain.example.com']);
function sanitizeHeaderValue(value) {
if (!value) return '';
// Remove CR, LF, and control characters
return value.replace(/[\x00-\x1f\x7f]/g, '');
}
function validateRedirectUrl(url) {
if (!url) return null;
// Check for CRLF
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('Invalid redirect URL');
}
res.redirect(safeUrl);
});
app.get('/download', (req, res) => {
const filename = req.query.filename || '';
// Sanitize filename
const safeFilename = sanitizeFilename(filename);
if (!safeFilename) {
return res.status(400).send('Invalid filename');
}
// Use proper encoding
const encodedFilename = encodeURIComponent(safeFilename);
res.setHeader('Content-Disposition',
`attachment; filename*=UTF-8''${encodedFilename}`);
res.send(fileContent);
});
function sanitizeFilename(filename) {
if (!filename) return null;
// Remove path
const path = require('path');
filename = path.basename(filename);
// Remove control characters
filename = filename.replace(/[\x00-\x1f\x7f]/g, '');
// Validate
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
return null;
}
return filename;
}
// Middleware to sanitize all headers
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();
});
Exploited in the Wild
HTTP Response Splitting
Cache poisoning through header injection.
Session Fixation
Injecting Set-Cookie headers for session attacks.
XSS via Headers
Cross-site scripting through manipulated headers.
Tools to test/exploit
-
Burp Suite — CRLF injection testing.
-
OWASP ZAP HTTP header fuzzing.
-
Manual testing with %0d%0a sequences.
CVE Examples
-
CVE-2019-14234: Django header injection.
-
Numerous CRLF injection CVEs in web frameworks.
References
-
MITRE. "CWE-644: Improper Neutralization of HTTP Headers for Scripting Syntax." https://cwe.mitre.org/data/definitions/644.html
-
OWASP. "HTTP Response Splitting."