Header Injection in Express
How Header Injection Manifests in Express
Header injection in Express applications typically occurs when user-controlled data flows into HTTP response headers without proper validation. This vulnerability can lead to HTTP response splitting, cache poisoning, and cross-site scripting attacks. Express's middleware architecture and request handling create specific attack surfaces that developers must understand.
The most common Express-specific scenario involves dynamic header setting based on request parameters. Consider this vulnerable pattern:
app.get('/set-header', (req, res) => {
const headerName = req.query.name;
const headerValue = req.query.value;
res.set(headerName, headerValue);
res.send('Header set');
});
An attacker can exploit this by sending: /set-header?name=Content-Type&value=text/html%0d%0a%0d%0a%3Cscript%3Ealert(1)%3C/script%3E. The %0d%0a sequences represent CRLF characters that break out of the header section and inject malicious content into the response body.
Another Express-specific pattern involves using res.redirect() with user input:
app.get('/redirect', (req, res) => {
res.redirect(req.query.url);
});
This allows open redirect attacks and can be combined with header injection if the redirect URL contains CRLF sequences.
Express's res.append() method also creates injection risks when used with unsanitized input:
app.get('/append-header', (req, res) => {
res.append('X-Custom-Header', req.query.data);
res.send('Appended');
});
The vulnerability extends to template rendering where headers might be set dynamically based on template variables. Express's default view engine behavior doesn't automatically escape header values, making this a common oversight.
Middleware order matters significantly. If header-setting middleware runs before authentication checks, an attacker might inject headers that affect authentication flows or bypass security controls.
Express-Specific Detection
Detecting header injection in Express requires both static code analysis and dynamic testing. For static analysis, look for patterns where request parameters directly influence header values without validation.
Code review should identify:
- Direct use of
req.query,req.params, orreq.bodyinres.set(),res.append(), orres.header() - Dynamic header names set from user input
- URL parameters passed to
res.redirect() - Template variables used in header contexts
Dynamic testing with tools like middleBrick specifically scans for header injection vulnerabilities by sending payloads containing CRLF sequences and observing responses. middleBrick's black-box scanning approach tests the actual running application without requiring source code access.
For Express applications, middleBrick performs these specific checks:
middlebrick scan https://yourapp.com --tests=header-injection
The scanner sends requests with encoded CRLF sequences (%0D%0A) in various parameters and headers, then analyzes responses for:
- Unexpected headers appearing in responses
- Response body content injection
- HTTP response splitting where status codes or locations are manipulated
- Cache poisoning indicators where injected headers affect caching behavior
middleBrick's API security scanning also checks for related issues like open redirects and unsafe consumption patterns that often accompany header injection vulnerabilities. The tool provides a security score (A-F) and specific findings with severity levels and remediation guidance.
During development, middleware like helmet can help detect potential header injection by enforcing strict header policies:
const helmet = require('helmet');
app.use(helmet({
hsts: false // disable if not needed
}));
Helmet's content security policies and other protections can prevent some header injection impacts even when the underlying vulnerability exists.
Express-Specific Remediation
Remediating header injection in Express requires input validation, output encoding, and architectural changes to prevent unsafe data flows. The most effective approach combines multiple defense layers.
Input validation should be the first line of defense. Use a whitelist approach for header names and strict validation for header values:
const validHeaders = ['X-Custom-Header', 'X-Tracking-ID'];
app.get('/set-header', (req, res) => {
const headerName = req.query.name;
const headerValue = req.query.value;
if (!validHeaders.includes(headerName)) {
return res.status(400).send('Invalid header name');
}
if (/
|
/.test(headerValue)) {
return res.status(400).send('Header value contains invalid characters');
}
res.set(headerName, headerValue);
res.send('Header set');
});
The regular expression /
|
/ detects carriage return and line feed characters that enable header injection. Always validate against CRLF sequences before using user input in headers.
For redirect functionality, implement a safe redirect mechanism:
const allowedRedirects = ['https://example.com', 'https://yourapp.com'];
app.get('/redirect', (req, res) => {
const url = req.query.url;
if (!allowedRedirects.includes(url)) {
return res.status(400).send('Invalid redirect URL');
}
res.redirect(url);
});
Alternatively, use relative redirects or a URL validation library to ensure redirects stay within your domain.
Express's built-in res.location() method provides some protection, but always validate the input:
app.post('/callback', (req, res) => {
const returnUrl = req.query.returnUrl || '/default';
// Validate returnUrl is relative or in allowed list
if (returnUrl.startsWith('http') && !allowedRedirects.some(domain => returnUrl.startsWith(domain))) {
return res.status(400).send('Invalid return URL');
}
res.location(returnUrl);
res.send('Processing complete');
});
For applications using template engines, ensure header values are properly escaped. With Pug/Jade:
//- Bad: unescaped header value
- res.set('X-Custom', headerValue)
//- Good: validate before setting
- var safeValue = headerValue.replace(/[^\w\s-]/g, '')
- res.set('X-Custom', safeValue)
Middleware-based validation provides centralized protection:
function validateHeaders(req, res, next) {
const suspiciousHeaders = Object.keys(req.headers).filter(header =>
/\r|\n/.test(header) || /\r|\n/.test(req.headers[header])
);
if (suspiciousHeaders.length > 0) {
console.warn('Suspicious headers detected:', suspiciousHeaders);
return res.status(400).send('Invalid request');
}
next();
}
app.use(validateHeaders);
Finally, implement comprehensive logging and monitoring. Log all header-setting operations with user context, making it easier to detect and investigate potential injection attempts.