HTTP header injection can lead to dangerous response splitting attacks. Learn how to secure your Node.js and PHP apps by implementing strict input validation.
During a recent security audit of a legacy Node.js application, I found a seemingly harmless feature that allowed users to set a custom "Location" header for a redirect. It took about 45 minutes to realize that the raw input was being passed directly into the response stream, creating a classic entry point for response splitting.
If you aren't careful, your web server might interpret malicious newline characters in user input as the end of a header, allowing an attacker to inject their own headers or even a completely fake HTTP response body.
HTTP header injection happens when you allow untrusted data to influence the headers your server sends back to the client. The real danger, however, is response splitting. By injecting \r\n (CRLF) sequences, an attacker can prematurely terminate the header section.
If an attacker injects \r\n\r\n, the browser or a proxy server thinks the headers have ended, and whatever the attacker puts next is treated as the start of a new, malicious HTTP response. This can lead to XSS, cache poisoning, or session hijacking. I’ve seen this happen when developers assume that input is "just text" and forget that headers are strictly structured by line breaks.
We first tried to solve this by simply stripping \r and \n characters using a basic regex replace. It worked for simple cases, but it failed when we encountered double-encoding or specific character sets that bypassed our filter.
We realized that instead of trying to sanitize, we should strictly whitelist. If a header value contains anything that isn't a standard alphanumeric character or a safe delimiter, we should reject the request entirely. This is a core tenet of security best practices that we often overlook when we're in a hurry to ship features.
In Node.js (specifically with Express), you might be tempted to do this:
JAVASCRIPT// DANGEROUS: Do not do this app.get(CE9178">'/redirect', (req, res) => { res.setHeader(CE9178">'Location', req.query.url); res.redirect(302, req.query.url); });
The res.setHeader method is vulnerable if req.query.url contains CRLF. The fix is to validate the input against a strict pattern before it ever touches the response object.
JAVASCRIPTconst safeUrlPattern = /^[a-zA-Z0-9\/\-\_\.\?\=\&]+$/; app.get(CE9178">'/redirect', (req, res) => { const url = req.query.url; if (!safeUrlPattern.test(url)) { return res.status(400).send(CE9178">'Invalid redirect URL'); } res.redirect(302, url); });
By enforcing this, you eliminate the possibility of an attacker injecting newlines. If you're building more complex systems, consider how this interacts with other vulnerabilities, such as preventing arbitrary file write vulnerabilities in Node.js and PHP.
PHP is historically prone to this because the header() function doesn't automatically sanitize input. If you pass a string with a newline to header(), it will happily split the response.
PHP#6A9955">// DANGEROUS: Potential for response splitting header("Location: " . $_GET['url']);
To fix this, you must explicitly check for CRLF characters before calling the function.
PHP$url = $_GET['url']; if (preg_match("/[\r\n]/", $url)) { http_response_code(400); die("Invalid header value"); } header("Location: " . $url);
It’s a simple check, but it’s easy to forget when you’re maintaining a large codebase. Much like preventing integer overflow and underflow in Node.js and PHP, the key is to assume the input is malicious until proven otherwise.
If you take one thing away from this, let it be this: never trust the input when constructing headers. Here are three rules I follow:
If you're dealing with more complex attack vectors like command injection in Node.js, you know that defense-in-depth is the only way to sleep soundly.
No. HTTPS encrypts the traffic, but the browser still decodes the HTTP headers and body. If the server sends a split response, the browser will still process it as two separate responses.
No. Any header that reflects user input—like Set-Cookie, Content-Disposition, or custom headers—is a potential target for HTTP header injection.
Stripping characters is okay, but rejecting the request is better. If someone is sending you a newline in a field that shouldn't have one, they’re likely testing your security boundaries. Don't accommodate them.
I'm still refining our approach to header validation. Sometimes, we need to allow complex URLs, and our regex gets messy. Next time, I'm planning to move toward a dedicated URL-parsing library that handles validation more robustly than a manual regex. Always be skeptical of your own "simple" fixes.
TOCTOU race conditions leave your file system operations vulnerable. Learn how to secure your Node.js and PHP code against these concurrency flaws today.
Read moreStop arbitrary file write attacks by implementing strict validation and secure storage. Learn how to protect your Node.js and PHP apps from file overwrites.