CRLF Injection and HTTP Response Splitting: What Developers Miss

CRLF injection lets attackers insert carriage return and line feed characters into HTTP headers and log files, enabling HTTP response splitting, cache poisoning, session fixation, and log forgery. This guide explains the mechanics, shows exploits in Python and Node.js, and covers the fixes.

HTTP uses carriage return followed by line feed (\r\n) as the delimiter between headers, and as the separator between headers and body. That design is three decades old, and it becomes a vulnerability whenever an application includes user-controlled input in an HTTP response header without stripping those characters.

The attack has a name — CRLF injection, also called HTTP response splitting when the injected characters create artificial response boundaries — and it enables a cluster of secondary attacks: cache poisoning, XSS, session fixation, and log forgery. It shows up regularly in penetration tests despite being well understood, because developers think about sanitising values for HTML output and SQL queries but rarely for HTTP header values.

The Mechanics

HTTP headers look like this:

HTTP/1.1 302 Found\r\n
Location: https://example.com/page\r\n
Set-Cookie: session=abc123\r\n
\r\n

The \r\n sequences are header terminators. A double \r\n marks the boundary between headers and body. If you inject a \r\n inside a header value, you create a new header. If you inject \r\n\r\n, you end the header section and start an arbitrary body.

A vulnerable redirect endpoint:

from flask import Flask, redirect, request

app = Flask(__name__)

@app.route('/redirect')
def unsafe_redirect():
    url = request.args.get('url', '/')
    return redirect(url)  # url is included in Location header

An attacker crafts this request:

GET /redirect?url=https://example.com%0d%0aSet-Cookie:+session=attacker_session HTTP/1.1

The %0d%0a URL-decodes to \r\n. The resulting response header:

HTTP/1.1 302 Found
Location: https://example.com
Set-Cookie: session=attacker_session

The server just set a cookie on behalf of the attacker.

Attack: HTTP Response Splitting

With a \r\n\r\n injection, an attacker can append a complete fake response body:

GET /redirect?url=https://example.com%0d%0a%0d%0a<html><script>alert(document.cookie)</script></html> HTTP/1.1

Resulting response:

HTTP/1.1 302 Found
Location: https://example.com

<html><script>alert(document.cookie)</script></html>

In practice, modern browsers follow the redirect and ignore the injected body. The real danger is in intermediate HTTP caches. If the forged response reaches a caching proxy, the proxy may cache the injected body as the canonical response for the target URL. Subsequent users requesting that URL receive the attacker’s content — this is cache poisoning.

Attack: Log Injection

Log injection is the same vulnerability applied to log files rather than HTTP responses. If user input appears in application logs without sanitisation, an attacker can inject fake log entries:

import logging

logger = logging.getLogger(__name__)

@app.route('/login', methods=['POST'])
def login():
    username = request.form.get('username')
    logger.info(f"Login attempt for user: {username}")
    # ...

An attacker submits a username of:

admin\nINFO 2026-06-28 Login successful for user: admin\nINFO 2026-06-28 Privilege escalated to root

The resulting log file contains fabricated entries indistinguishable from legitimate ones. This is used to cover tracks, confuse incident response, and inject false entries that trigger or suppress automated alerting.

Vulnerable Patterns in Node.js/Express

Express does sanitise headers in recent versions, but older code and lower-level frameworks do not:

// Vulnerable in older versions of Express or http module directly
const http = require('http');

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://localhost');
  const redirectTarget = url.searchParams.get('to');
  
  // Direct header injection — vulnerable
  res.writeHead(302, { 'Location': redirectTarget });
  res.end();
}).listen(3000);
// Also vulnerable: setting custom headers from user input
app.get('/api/data', (req, res) => {
  const format = req.query.format;
  res.setHeader('Content-Type', `application/${format}`);  // CRLF injectable
  res.send(data);
});

Fixes

Strip or encode CRLF characters from all values placed in HTTP headers.

In Python:

import re

def sanitise_header_value(value: str) -> str:
    # Remove all CR and LF characters
    return re.sub(r'[\r\n]', '', value)

@app.route('/redirect')
def safe_redirect():
    url = request.args.get('url', '/')
    safe_url = sanitise_header_value(url)
    return redirect(safe_url)

For redirects specifically, validate the URL is to an allowed domain before redirecting:

from urllib.parse import urlparse

ALLOWED_REDIRECT_DOMAINS = {'example.com', 'app.example.com'}

def safe_external_redirect(url: str):
    parsed = urlparse(url)
    if parsed.netloc not in ALLOWED_REDIRECT_DOMAINS:
        return redirect('/')
    safe_url = re.sub(r'[\r\n]', '', url)
    return redirect(safe_url)

In Node.js, use the encodeURI or validate explicitly:

const { URL } = require('url');

function safeRedirect(res, target) {
  // Validate URL is to an allowed origin
  try {
    const parsed = new URL(target);
    const allowed = ['https://example.com', 'https://app.example.com'];
    if (!allowed.some(origin => parsed.origin === origin)) {
      res.redirect('/');
      return;
    }
    // Strip CRLF just in case
    const safe = target.replace(/[\r\n]/g, '');
    res.redirect(safe);
  } catch {
    res.redirect('/');
  }
}

For logging, encode newlines in user input before writing:

def safe_log_value(value: str) -> str:
    return value.replace('\n', '\\n').replace('\r', '\\r')

logger.info(f"Login attempt for user: {safe_log_value(username)}")

Structured logging libraries (structlog, python-json-logger, Winston in Node.js) serialise values as JSON strings, which naturally escape newlines. Switching to structured logging is the most durable fix for log injection.

Where to Find It

CRLF injection reliably appears in:

  • Redirect endpoints that include url, next, return_to, or redirect parameters in the Location header
  • Cookie-setting code that constructs Set-Cookie values from user input (custom cookie names or values)
  • Custom response headers that reflect request parameters (Content-Disposition: attachment; filename=<user_input>)
  • Logging middleware that includes request headers, user-agent strings, or query parameters in log output without encoding

Automated scanners (Burp Suite, OWASP ZAP) detect basic CRLF injection by injecting %0d%0a in parameters and observing whether response headers change. Manual testing adds %0a, %0d, \r\n, and Unicode variants (%E5%98%8A%E5%98%8D) against redirect and cookie-setting endpoints.

References