Security Reference
MCP server HTTP header injection: CRLF injection, response splitting, and redirect forgery
HTTP header injection exploits the fact that response headers are CR+LF delimited. If user-controlled input reaches a response header value without stripping \r\n, an attacker can forge additional headers — injecting Set-Cookie, overriding Content-Type, or splitting the HTTP response into two entirely separate responses.
The CRLF injection mechanism
HTTP/1.1 separates header lines with carriage-return + line-feed (\r\n). A response header block ends with a blank line (\r\n\r\n). If user input containing \r\n is embedded in a header value, the server appears to emit multiple headers — or terminates the header block early and starts a new body:
// VULNERABLE: redirect URL from user input — no CR/LF stripping
app.get('/mcp/auth/callback', (req, res) => {
const returnTo = req.query.return_to;
// If returnTo = "/home\r\nSet-Cookie: session=attacker"
// The response header block becomes:
// Location: /home
// Set-Cookie: session=attacker
res.redirect(returnTo); // Express doesn't strip CRLF before Node 14.18
});
// SAFE: strip CR/LF before embedding in any header value
function sanitizeHeaderValue(val) {
return String(val).replace(/[\r\n]/g, '');
}
app.get('/mcp/auth/callback', (req, res) => {
const rawReturnTo = req.query.return_to || '/';
const returnTo = sanitizeHeaderValue(rawReturnTo);
// Also validate it's a relative path — don't allow open redirects
if (!returnTo.startsWith('/') || returnTo.startsWith('//')) {
return res.redirect('/');
}
res.redirect(returnTo);
});
Response splitting
Response splitting is CRLF injection carried further. By injecting a full \r\n\r\n sequence the attacker terminates the header block and controls the response body — allowing HTML injection for shared-proxy attacks:
// Injected input: "/page\r\n\r\nFake page with malware script"
// Result:
// HTTP/1.1 302 Found
// Location: /page
//
// Fake page with malware script
// HTTP/1.1 200 OK ← second fabricated response follows in the stream
// Content-Type: text/html
// ...legitimate response...
// Modern Node.js (v14.18+) and Express v4+ throw on CRLF in headers:
// Error: Invalid character in header content
// — but older versions silently pass through, and some reverse proxies
// normalize responses before forwarding, allowing the split to survive
Dependency risk: If your MCP server runs behind a reverse proxy (nginx, Caddy) that terminates HTTP/1.1 and re-encodes to HTTP/2, the response splitting mechanism differs. HTTP/2 uses binary framing with no CRLF delimiters, but CRLF payloads can still corrupt logs and intermediate caches. Sanitize regardless of HTTP version.
Redirect header injection
MCP servers that implement OAuth flows, capability negotiation, or PKCE redirects frequently set Location headers based on request parameters. Validate strictly:
const ALLOWED_REDIRECT_HOSTS = new Set([
'mcp.myserver.com',
'localhost',
'127.0.0.1'
]);
function safeRedirectURL(raw) {
// Strip CRLF
const cleaned = String(raw || '').replace(/[\r\n\t]/g, '');
// Relative paths: allow
if (cleaned.startsWith('/') && !cleaned.startsWith('//')) {
return cleaned;
}
// Absolute URLs: only allowed hosts
try {
const u = new URL(cleaned);
if (!ALLOWED_REDIRECT_HOSTS.has(u.hostname)) {
throw new Error('Redirect host not allowed');
}
// Only allow https (no javascript:, data:, etc.)
if (u.protocol !== 'https:' && u.protocol !== 'http:') {
throw new Error('Redirect scheme not allowed');
}
return cleaned;
} catch {
return '/'; // Fallback to root on any validation failure
}
}
app.get('/mcp/oauth/callback', (req, res) => {
const target = safeRedirectURL(req.query.redirect_uri);
res.redirect(302, target);
});
Content-Type injection via filename reflection
MCP file tools that set Content-Disposition with user-controlled filenames are vulnerable to header injection through the filename parameter:
// VULNERABLE: filename from user input embedded in Content-Disposition
app.get('/mcp/download', (req, res) => {
const filename = req.query.name;
// Input: "report.pdf\r\nContent-Type: text/html\r\nX-Injected: yes"
res.set('Content-Disposition', `attachment; filename="${filename}"`);
res.send(fileContent);
});
// SAFE: strip CRLF, quote-encode the filename, strip path separators
function sanitizeFilename(raw) {
return String(raw)
.replace(/[\r\n\t"\\\/]/g, '_') // strip unsafe chars
.slice(0, 255); // length cap
}
app.get('/mcp/download', (req, res) => {
const filename = sanitizeFilename(req.query.name || 'file');
res.set({
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Type': 'application/octet-stream' // explicit — never inherit
});
res.send(fileContent);
});
Log injection via reflected headers
MCP servers commonly log the User-Agent, X-Request-ID, or Referer headers for debugging. These fields accept arbitrary strings from the client. A CRLF payload in a logged header can forge log entries, corrupt log parsers, or inject ANSI escape codes that produce misleading terminal output:
// VULNERABLE: logging raw User-Agent header
app.use((req, _res, next) => {
console.log(`[MCP] ${req.method} ${req.path} ua=${req.headers['user-agent']}`);
// Input: "claude-code\r\n[MCP] POST /mcp/delete-all ua=attacker"
// Log shows a fabricated delete-all line with attacker identity
next();
});
// SAFE: sanitize logged header values
function sanitizeLogValue(val, maxLen = 200) {
return String(val || '')
.replace(/[\r\n\x00-\x1f\x7f]/g, ' ') // strip control chars
.slice(0, maxLen);
}
app.use((req, _res, next) => {
const ua = sanitizeLogValue(req.headers['user-agent']);
const reqId = sanitizeLogValue(req.headers['x-request-id']);
console.log(`[MCP] ${req.method} ${req.path} ua=${ua} rid=${reqId}`);
next();
});
SkillAudit grading criteria
| Finding | Severity | Score impact |
|---|---|---|
| User input embedded in Location/Set-Cookie/Content-Disposition without CRLF stripping | HIGH | −18 |
| Open redirect via user-controlled Location header | HIGH | −15 |
| Raw request headers logged without control-character sanitization | MEDIUM | −8 |
| User-controlled filename in Content-Disposition without encoding | MEDIUM | −8 |
| CRLF stripping on all header values derived from user input | PASS | +6 |
| Redirect allowlist limiting Location to known origins | PASS | +5 |
Related SkillAudit checks
- DNS rebinding security — header injection can forge Host header to bypass DNS rebinding defenses
- Log injection security — CRLF in logs is a subset of the broader log injection problem
- Cache poisoning — injected headers can create unexpected cache key variants
- Input validation patterns — header value sanitization fits the semantic allow-list layer