HTTP Header Injection: Unpacking CRLF Vulnerabilities and Response Splitting
The HTTP protocol is the invisible backbone of the modern web. Every time a user navigates to a webpage, logs into a portal, or submits a form, a series of HTTP requests and responses orchestrate the transaction. While most security discourse naturally gravitates toward vulnerabilities inside the application logic or the payload body—like Cross-Site Scripting (XSS) or SQL Injection—a more subtle, yet profoundly dangerous class of vulnerabilities exists within the very metadata of these web transactions.
Enter HTTP Header Injection, more formally known in the security community as CRLF Injection.
HTTP Header Injection occurs when an attacker can insert arbitrary HTTP headers into a web application's response by manipulating user-supplied input. While it might sound like a niche or localized issue, the repercussions of successful header injection are systemic. It can lead to HTTP Response Splitting, Web Cache Poisoning, Cross-Site Scripting (XSS), and Session Hijacking.
In this comprehensive guide, we will dissect the mechanics of HTTP Header Injection, explore its devastating real-world consequences, examine vulnerable code samples across different programming languages, and establish a robust framework for defending your applications.
The Core Mechanism: Understanding CRLF
To truly grasp how HTTP Header Injection works, we must first look at how the HTTP protocol separates its internal components.
HTTP is a text-based protocol. When a client (like a web browser) sends a request to a server, or when a server sends a response back to the client, the data is structured into two main sections:
The Headers: Metadata about the request or response (e.g., Content-Type, Set-Cookie, Location).
The Body: The actual payload (e.g., the HTML content of a page, JSON data).
The HTTP specification (RFC 7230) dictates that each line in the HTTP header section must be terminated by a specific sequence of non-printable characters: Carriage Return (CR) followed by Line Feed (LF).
In ASCII, these characters are represented as:
CR:\r (Hex: 0x0D, URL-encoded: %0D)
LF:\n (Hex: 0x0A, URL-encoded: %0A)
Therefore, the sequence \r\n (or %0d%0a in an encoded URL) acts as the definitive boundary between different headers. Furthermore, the HTTP specification states that the header section is entirely separated from the body section by two consecutive CRLF sequences (\r\n\r\n).
The Injection Point
The vulnerability arises when a web application takes user-supplied input—such as a URL parameter, form data, or a cookie—and embeds it directly into an HTTP response header without proper sanitization or encoding.
If an attacker inputs the %0d%0a sequence into the vulnerable parameter, the web server interprets these characters exactly as the HTTP protocol demands: as the end of the current header line and the beginning of a fresh one. By doing so, the attacker successfully "injects" their own malicious headers into the response.
Anatomy of the Vulnerability
Let's visualize this with a simple scenario. Imagine a website that tracks the language preference of a user via a URL parameter and sets a cookie based on that preference.
The Intended Interaction:
The user visits: https://example.com/set-lang?lang=en
The application's backend code takes the lang parameter and constructs an HTTP response that looks like this:
HTTP/1.1 200 OKContent-Type: text/htmlSet-Cookie: language=enContent-Length: 45<html><body>Language set to en</body></html>
The Malicious Interaction:
An attacker crafts a malicious URL, appending a URL-encoded CRLF sequence followed by a new header:
https://example.com/set-lang?lang=en%0d%0aSet-Cookie:%20session_id=hacked123
Because the application blindly incorporates the lang parameter into the Set-Cookie header, the raw HTTP response sent back to the browser looks like this:
HTTP/1.1 200 OKContent-Type: text/htmlSet-Cookie: language=enSet-Cookie: session_id=hacked123Content-Length: 45<html><body>Language set to en</body></html>
The application intended to send one cookie. The attacker, by utilizing the \r\n characters embedded in %0d%0a, forced the server to process a secondSet-Cookie header. The attacker has successfully dictated the HTTP state.
The Domino Effect: Consequences and Real-World Impact
HTTP Header Injection is a "gateway" vulnerability. Its true danger lies not just in adding a stray header, but in the chain of secondary attacks it enables. Here are the most critical consequences of a CRLF flaw.
A. HTTP Response Splitting
HTTP Response Splitting is the most severe escalation of CRLF injection. If an attacker can inject two consecutive CRLF sequences (\r\n\r\n), they signal to the web browser (or an intermediary caching proxy) that the HTTP headers have concluded and the HTTP body has begun.
But it doesn't stop there. By continuing to inject content, the attacker can entirely forge a secondary, secondary HTTP response hidden within the payload of the first.
The Exploit:
An attacker sends the payload:
%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0aContent-Length:%2025%0d%0a%0d%0a<script>alert(1)</script>
The Server's Perspective:
The server responds with what it believes is a single stream of characters.
The Intermediary/Browser's Perspective:
A proxy server or the victim's browser receives the stream, parses the injected \r\n\r\n, and believes the server sent two separate HTTP responses.
The first response is abruptly terminated. The second response is fully controlled by the attacker, complete with a malicious HTML body containing a Cross-Site Scripting (XSS) payload.
B. Web Cache Poisoning
When HTTP Response Splitting is weaponized against a shared caching layer (like a CDN, Varnish, or a corporate proxy), the impact scales from targeting a single user to compromising the entire user base.
If an attacker sends a carefully crafted Response Splitting payload through a caching server, the cache might store the attacker's forged "second response" and associate it with a legitimate URL. When subsequent, innocent users request that legitimate URL, the caching server serves them the attacker's malicious, cached payload instead of routing the request to the backend web server.
This effectively defaces the website or serves systemic persistent XSS to all visitors without ever altering the backend database or source code.
C. Cross-Site Scripting (XSS) via Header Injection
Even without achieving full Response Splitting, an attacker can achieve XSS by injecting specific headers. For instance, if an application lacks a protective Content-Type header, or if the attacker can overwrite it, they can force the browser to interpret the response as HTML.
By injecting %0d%0aContent-Type: text/html%0d%0a%0d%0a<script>alert('XSS')</script>, the attacker overrides the browser's rendering engine, forcing it to execute their JavaScript in the victim's session context.
D. Session Fixation and Hijacking
As demonstrated in our earlier anatomy example, an attacker can inject a Set-Cookie header. If they inject a known session ID (Session Fixation), they can trick the victim into authenticating under a session the attacker controls. Once the victim logs in, the attacker can use that same session ID to access the victim's account.
Furthermore, attackers can inject headers to bypass security boundaries, such as injecting Access-Control-Allow-Origin: * to circumvent Cross-Origin Resource Sharing (CORS) policies, allowing their malicious external sites to read sensitive data from the vulnerable domain.
Real-World Scenarios and Case Studies
Scenario 1: The Vulnerable Redirection Mechanism
A highly common vector for CRLF injection is the HTTP Location header, which is used for 301 and 302 redirects. Let's look at an application that takes a next parameter to redirect a user after login.
By simply altering a redirect parameter, the attacker has elevated their privileges by forcing the server to issue an Admin cookie.
Scenario 2: Log Injection / Forging
CRLF vulnerabilities aren't strictly limited to HTTP headers. They frequently manifest as Log Injection. When a web server logs a user's User-Agent or specific URL parameters to a flat text file, each log entry is usually separated by a newline.
If an application logs an attacker's payload Login Failed for User: admin%0d%0aLogin Succeeded for User: admin, the resulting log file will look like this:
2026-03-06 10:00:01 - Login Failed for User: adminLogin Succeeded for User: admin
The attacker has forged a critical log entry, potentially derailing incident response investigations or triggering automated monitoring triggers maliciously.
Code Samples: The Vulnerable vs. The Secure
To build resilient systems, developers must know what vulnerable code looks like. Let's examine how HTTP Header Injection manifests across popular languages and frameworks, and exactly how to fix it.
Node.js (Express Framework)
Vulnerable Code:
In older versions of Node.js (prior to version 8.x where strict header validation was enforced at the core HTTP level), or when manually writing HTTP streams, header injection was trivial. Here is a conceptual vulnerable snippet:
const express = require('express');const app = express();app.get('/set-theme', (req, res) => { // VULNERABLE: Trusting user input directly in a header const userTheme = req.query.theme; // Setting a custom header using the unvalidated input res.set('X-User-Theme', userTheme); res.send('Theme preference saved!');});app.listen(3000);
If theme is dark%0d%0aSet-Cookie:%20session=stolen, the Express res.set() will push the injected cookie.
Secure Code:
Modern Express and Node.js mitigate this aggressively by throwing a TypeError: The header content contains invalid characters if \r or \n is detected. However, best practice demands application-level validation:
Vulnerable Code:
If an application manually constructs response objects utilizing untrusted input, it risks CRLF injection.
from flask import Flask, request, make_responseapp = Flask(__name__)@app.route('/download')def download(): # VULNERABLE: Taking filename from user input without sanitization filename = request.args.get('file', 'default.pdf') response = make_response("File contents here...") # Injecting unvalidated input into the Content-Disposition header response.headers['Content-Disposition'] = f"attachment; filename={filename}" return response
An attacker passing file=test.pdf%0d%0aX-Injected:%20True dictates the headers.
Secure Code:
Sanitization is required. Python's werkzeug (the core utility of Flask) often escapes invalid characters automatically in newer versions, but explicitly sanitizing or URL-encoding output remains critical.
import urllib.parsefrom flask import Flask, request, make_responseapp = Flask(__name__)@app.route('/download')def download(): filename = request.args.get('file', 'default.pdf') # SECURE: Strip all carriage returns and line feeds before using the string sanitized_filename = filename.replace('\r', '').replace('\n', '') # Alternatively, URL-encode the header value if the client expects it # encoded_filename = urllib.parse.quote(filename) response = make_response("File contents here...") response.headers['Content-Disposition'] = f"attachment; filename={sanitized_filename}" return response
PHP (Native)
Vulnerable Code:
PHP's native header() function has historically been highly susceptible to this attack, leading to massive Response Splitting vulnerabilities in the early web era.
<?php// VULNERABLE: Direct injection into the header function$redirect_url = $_GET['url'];header("Location: " . $redirect_url);exit();?>
Secure Code:
Modern PHP (since version 5.1.2) actively blocks multiple headers sent via a single header() call, effectively killing HTTP Response Splitting. However, single header modification can still occur if improperly formatted. The paramount defense relies on strict URL validation.
<?php$redirect_url = $_GET['url'];// SECURE: Validate that the URL is legitimate and well-formedif (filter_var($redirect_url, FILTER_VALIDATE_URL)) { // SECURE: Strip any stray newline characters just in case $safe_url = str_replace(array("\r", "\n"), '', $redirect_url); header("Location: " . $safe_url); exit();} else { die("Invalid redirect destination.");}?>
Best Protection Mechanisms and Defense-in-Depth
Securing an application against HTTP Header Injection requires a layered, defense-in-depth approach. You cannot rely solely on the underlying web server or framework to magically sanitize all inputs; your application code must be proactive.
A. Strict Input Validation (The Allow-list Approach)
The absolute best defense against CRLF injection—and indeed, most injection attacks—is strict input validation using an allow-list approach.
Whenever user input is destined for an HTTP header (such as a redirect URL, a cookie value, or a custom X- header), the application should verify that the input matches an expected, highly constrained format. If the input expects an alphanumeric username, ensure it only contains a-zA-Z0-9. Drop or reject any input containing shell characters, spaces, or especially \r and \n. Never attempt to create a "deny-list" of bad characters, as attackers will inevitably find encoding bypasses.
B. Header Parameter Sanitization and Encoding
In scenarios where user input must be placed into a header and an allow-list is impossible (e.g., dynamic filenames in Content-Disposition), the application code must aggressively strip or safely encode the input.
Stripping: Programmatically execute .replace('\r', '') and .replace('\n', '') on the input string before appending it to the header object.
Encoding: URL-encode the data. If the input malicious\r\n is URL encoded, it becomes malicious%0D%0A. When placed in the header, the server treats it as literal text rather than an execution control character, rendering the injection inert.
C. Framework and Environment Upgrades
One of the most effective mitigations is simply keeping your technology stack modernized. The threat of HTTP Response Splitting forced the creators of Java (Tomcat), Node.js, Python, and PHP to modify their low-level standard libraries years ago.
Modern runtimes actively inspect strings passed to header manipulation functions. If they detect a \r or \n embedded in the header value, they will throw an unhandled exception (e.g., ValueError in Python or TypeError in Node.js) and crash the request rather than emitting a malformed HTTP response. Ensuring you are not running end-of-life language runtimes is fundamental hygiene.
D. Web Application Firewalls (WAF) and Ingress Controllers
Edge-level defenses provide a crucial, systemic safety net. A robust Web Application Firewall (WAF) deployed in front of your application load balancers can proactively scan incoming requests for the URL-encoded equivalents of carriage returns (%0d, %0a) located in anomalous parameters.
Furthermore, modern proxies and ingress controllers (like Nginx, HAProxy, and Envoy) enforce strict RFC 7230 compliance for HTTP syntax. They will often normalize requests, drop malformed headers, and prevent response splitting anomalies from successfully traversing the caching layer back down to the end-user.
Conclusion
HTTP Header Injection serves as a powerful reminder of the web's fragility. While it lacks the sheer notoriety of SQL Injection, its ability to manipulate the fundamental state mechanisms of the HTTP protocol allows attackers to execute devastating, multi-stage attacks like Web Cache Poisoning and Session Hijacking.
The defense against CRLF injection is remarkably straightforward but requires unwavering developer discipline. Treat HTTP headers with the exact same skepticism as database queries. By implementing rigorous input allow-lists, aggressively stripping newline control characters from untrusted data, and keeping up-to-date with modern, safely-designed web frameworks, engineering teams can entirely eradicate this vulnerability class and ensure the structural integrity of every HTTP transaction.