Skip to content

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%0a gives \r\n. The first CRLF terminates the Location: header. The second CRLF pair (after Content-Type: text/html) starts the response body, which the attacker fills with a script tag. The browser renders the injected HTML/JavaScript.
<?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\n closes the first Set-Cookie header and starts a second one with admin=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\n can inject additional headers or override the Content-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/\n bytes and their percent-encoded forms before the value reaches header(). basename() removes directory traversal sequences.
<?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 the Set-Cookie header 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() enables SameSite, Secure, and HttpOnly attributes.

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, %0a and Unicode line terminators using sanitizeHeaderValue() before any header() call
  • PRIORITY 3: Replace header("Set-Cookie: $userInput") with setcookie(name, sanitizedValue, options[])

Verify the Fix

  • Submit %0d%0aX-Injected: evil in redirect, filename, and cookie parameters; confirm the injected header does not appear in the response
  • Check php.ini for header_register_callback hooks 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.

Additional Resources