CWE-113: HTTP Response Splitting - PHP
Overview
HTTP Response Splitting in PHP occurs when user-supplied strings are written to HTTP headers without stripping CRLF characters (\r\n). PHP's native header() function sends a raw HTTP header line; older PHP versions, some SAPIs, custom header emitters, and proxy or framework integrations may split the response if decoded CRLF reaches the header value. Current supported PHP versions reject raw CR or LF in header() values, but code should still validate and normalize header data because decoded %0a/%0d sequences can reach non-native sinks or compatibility paths.
Primary Defence: Never pass user input directly to header(). Use an allowlist for redirect destinations, and strip CRLF sequences from any value that must appear in a header. Prefer setcookie() over header("Set-Cookie: ...") for cookies.
Common Vulnerable Patterns
Unsanitized Redirect with header()
<?php
// VULNERABLE - user controls the redirect destination
$target = $_GET['redirect'];
header("Location: $target");
exit;
// Attack: redirect=%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(1)</script>
// The injected body executes JavaScript in the victim's browser
Why this is vulnerable:
- Decoding
%0d%0agives\r\n. The first CRLF terminates theLocation:header. The second CRLF pair (afterContent-Type: text/html) starts the response body, which the attacker fills with a script tag. The browser renders the injected HTML/JavaScript.
User Input in Set-Cookie Value
<?php
// VULNERABLE - user-supplied value embedded in Set-Cookie
$preference = $_COOKIE['theme'] ?? $_GET['theme'];
header("Set-Cookie: theme=$preference; Path=/");
// Attack: theme=dark%0d%0aSet-Cookie:%20admin=1
Why this is vulnerable:
- The injected
\r\ncloses the firstSet-Cookieheader and starts a second one withadmin=1. The browser stores both cookies.
Unsanitized Content-Disposition
<?php
// VULNERABLE - filename from query string placed into Content-Disposition header
$filename = $_GET['filename'];
header("Content-Disposition: attachment; filename=\"$filename\"");
readfile(__DIR__ . '/downloads/report.pdf');
Why this is vulnerable:
- A filename containing
\r\ncan inject additional headers or override theContent-Type, enabling XSS or response smuggling.
Secure Patterns
Allowlist-Based Redirect
<?php
declare(strict_types=1);
$allowedRedirects = [
'home' => '/home',
'dashboard' => '/dashboard',
'profile' => '/profile',
];
$key = $_GET['redirect'] ?? 'home';
$target = $allowedRedirects[$key] ?? '/home';
// SECURE: target is always a hardcoded string from the allowlist
header("Location: $target");
http_response_code(302);
exit;
Why this works:
- The user provides a key (
home,dashboard) not the URL itself. The actual redirect destination is always a developer-controlled string from the allowlist array.
CRLF Sanitization Utility
<?php
function sanitizeHeaderValue(string $value): string {
// Strip raw CRLF and percent-encoded variants
$stripped = preg_replace('/\r|\n|%0[dD]|%0[aA]/', '', $value);
// Also strip Unicode line terminators
return preg_replace('/[\x{0085}\x{2028}\x{2029}]/u', '', $stripped);
}
// SECURE: sanitize before any header() call
$filename = sanitizeHeaderValue($_GET['filename'] ?? 'download');
$filename = basename($filename) ?: 'download'; // prevent path traversal too
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . $filename . '"');
readfile(__DIR__ . '/downloads/report.pdf');
Why this works:
- The regex strips both raw
\r/\nbytes and their percent-encoded forms before the value reachesheader().basename()removes directory traversal sequences.
Safe Cookie with setcookie()
<?php
$raw = $_GET['theme'] ?? 'light';
// SECURE: sanitize and use setcookie() (not header('Set-Cookie: ...'))
$theme = preg_replace('/[^\w\-]/', '', $raw); // only word chars and hyphens
setcookie('theme', $theme, [
'expires' => time() + 86400,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
]);
Why this works:
setcookie()constructs theSet-Cookieheader internally and properly encodes the value. Applying an allowlist regex before the call ensures only safe characters reach the cookie value.- Using the options array form of
setcookie()enablesSameSite,Secure, andHttpOnlyattributes.
Remediation Steps
Locate the Finding
- Source:
$_GET,$_POST,$_REQUEST,$_COOKIE, HTTP request headers - Sink:
header()called with any user-supplied string,setcookie()with user-controlled values
Apply the Fix
- PRIORITY 1: Replace
header("Location: $userInput")with an allowlist lookup: user selects a key, you output the URL - PRIORITY 2: Strip
\r,\n,%0d,%0aand Unicode line terminators usingsanitizeHeaderValue()before anyheader()call - PRIORITY 3: Replace
header("Set-Cookie: $userInput")withsetcookie(name, sanitizedValue, options[])
Verify the Fix
- Submit
%0d%0aX-Injected: evilin redirect, filename, and cookie parameters; confirm the injected header does not appear in the response - Check
php.iniforheader_register_callbackhooks that might re-introduce the bug - Rescan with the security scanner to confirm the finding is resolved
Check for Similar Issues
Search for: header(, setcookie(, $_GET, $_POST used in header-related strings
Testing
- Normal input: confirm allowed redirect keys, expected filenames, and cookie values still produce valid headers.
- Boundary input: test empty values, long filenames, and values containing spaces or quotes to confirm they are handled predictably.
- Malicious input: send
%0d%0aX-Injected:%20evil, raw CRLF bytes, and mixed-case encoded variants; verify no injected header or response body is emitted.