Topic: MCP server CRLF injection security
MCP server CRLF injection — response header splitting attacks
HTTP response headers are terminated by \r\n (CRLF — carriage return + line feed). When an MCP server reflects user-controlled values into HTTP response headers without stripping or encoding these characters, an attacker can inject additional headers — or entire synthetic HTTP responses — into the HTTP stream. This attack, called HTTP response splitting, enables cookie injection, cache poisoning, cross-site scripting, and session fixation. Modern Node.js and Python HTTP frameworks largely prevent this at the library level, but custom header construction code remains vulnerable.
How CRLF injection works
HTTP/1.1 headers are separated by \r\n and the header section ends at \r\n\r\n. If user-controlled content is placed in a header value and the value contains \r\n characters, the attacker controls where the next header begins:
// Vulnerable: reflects user input into a redirect Location header
app.get('/tools/authenticate', (req, res) => {
const returnUrl = req.query.return_url;
// returnUrl is user-controlled and not sanitized
res.setHeader('Location', returnUrl);
res.status(302).send();
});
An attacker sends:
GET /tools/authenticate?return_url=https://mcp.corp.internal/%0d%0aSet-Cookie:%20mcp_session=attacker_session%0d%0a HTTP/1.1
%0d%0a is the URL-encoded form of \r\n. The resulting response headers:
HTTP/1.1 302 Found
Location: https://mcp.corp.internal/
Set-Cookie: mcp_session=attacker_session
Content-Type: text/html
The browser receives an injected Set-Cookie header, setting the victim's session to the attacker's known value (session fixation). When the victim later authenticates, their session is overwritten — the attacker already knows the token.
Response splitting for cache poisoning
With a double CRLF injection (\r\n\r\n), an attacker can inject a complete synthetic HTTP response into the stream, poisoning shared caches:
# URL-encoded injection that inserts a full synthetic response
# %0d%0a = \r\n, %20 = space
GET /api/resource?id=123%0d%0aContent-Length:%2044%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Length:%2039%0d%0a%0d%0a%3cscript%3ealert(document.cookie)%3c/script%3e HTTP/1.1
Some HTTP/1.1 intermediary proxies interpret this as two responses: the first (real) response ending at the injected Content-Length: 44, and the second (synthetic) response beginning with the attacker's HTTP/1.1 200 OK. If the second synthetic response is cached for a popular URL, all subsequent users receive the XSS payload from the cache.
Where MCP servers are vulnerable
The specific MCP server code patterns that introduce CRLF injection risk:
1. Reflection in redirect URLs
// Pattern seen in OAuth callback handlers
app.get('/oauth/callback', (req, res) => {
const state = req.query.state; // contains return URL — user-controlled
// Vulnerable: state is reflected directly into Location
res.redirect(302, state);
});
// Fix: validate and allowlist the return URL before redirecting
const ALLOWED_RETURN_PATHS = ['/tools', '/dashboard', '/'];
app.get('/oauth/callback', (req, res) => {
const state = req.query.state;
const safeReturn = ALLOWED_RETURN_PATHS.includes(state) ? state : '/';
res.redirect(302, safeReturn);
});
2. Custom header construction from tool arguments
// Vulnerable: MCP server passes tool arguments into custom response headers
app.post('/tools/call', authenticate, async (req, res) => {
const { name, arguments: args } = req.body.params;
const result = await callTool(name, args);
// Dangerous: reflects tool name (attacker-controlled) into X-Tool-Called header
res.setHeader('X-Tool-Called', name);
res.json(result);
});
// Fix: sanitize before setting any response header
function sanitizeHeaderValue(value: string): string {
// Remove all CR, LF, and null bytes
return value.replace(/[\r\n\x00]/g, '');
}
res.setHeader('X-Tool-Called', sanitizeHeaderValue(name));
3. Log correlation headers
// Some MCP servers echo back correlation IDs for distributed tracing
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] || generateId();
// Vulnerable: echoes user-provided request ID
res.setHeader('X-Request-Id', requestId);
next();
});
// Fix: validate the request ID format before echoing
const REQUEST_ID_PATTERN = /^[a-zA-Z0-9\-]{8,64}$/;
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'];
const safeId = (requestId && REQUEST_ID_PATTERN.test(requestId))
? requestId
: generateId();
res.setHeader('X-Request-Id', safeId);
next();
});
Framework-level protections (and their limits)
Modern HTTP frameworks provide partial protection:
- Node.js http module (v0.10+): rejects header values containing
\ror\nwith aTypeError: Invalid character in header content. Express inherits this. The exception is not caught by most error handlers, so the request fails but the server does not crash. - Python's http.server: does not validate header values — vulnerable by default.
aiohttpandFastAPIstrip CRLF from header values as of recent versions, but this varies by configuration. - Go's net/http: strips
\r\nfrom header values silently, which prevents injection but also silently truncates legitimate values that happen to contain these bytes.
The Node.js TypeError means a vulnerable pattern still causes a request failure — visible in logs — but does not silently succeed. In HTTP/2 deployments, CRLF is not the header separator, so the attack vector is reduced (headers are binary-framed). However, deployments often involve an HTTP/1.1 edge proxy that downgrades to H/1.1 for origin communication, re-introducing the risk.
SkillAudit detection
SkillAudit's static analysis checks for:
res.setHeader()calls where the value is derived from request parameters, query strings, or body fields without prior sanitizationres.redirect()calls where the URL is user-controlled without allowlist validation- Correlation ID echo patterns where the echoed value is not validated against a safe format
- Python handler code that uses
response.headers[user_key] = user_valuepatterns
CRLF injection is a Medium-severity finding (High if the injection is in Set-Cookie or Location headers, where session fixation or cache poisoning impact is direct).
Check your MCP server's HTTP response headers for CRLF injection vectors.
Run a free audit →