Skip to content

CWE-113: HTTP Response Splitting - C# / ASP.NET

Overview

HTTP Response Splitting occurs when attackers inject CRLF characters into HTTP headers, potentially allowing them to inject additional headers or even inject response bodies. This is closely related to CWE-93 (CRLF Injection).

Primary Defence: Use ASP.NET Core's redirect methods (Redirect(), RedirectToAction()), Response.Cookies.Append() with CookieOptions, and ContentDispositionHeaderValue which automatically handle encoding and prevent header injection. When manual header manipulation is unavoidable, apply defense-in-depth: validate input against expected format (e.g., Url.IsLocalUrl() for Location headers), remove CRLF characters (.Replace("\r", "").Replace("\n", "").Replace("\0", "")), remove control characters, and URL-encode values. Prefer framework APIs over manual sanitization whenever possible.

Common Vulnerable Patterns

Direct Header Injection

// VULNERABLE - Direct header injection
Response.Headers.Add("Location", "/page?return=" + userInput);
Response.AddHeader("Set-Cookie", "session=" + sessionId);

// Attack example:
// Input: "value\r\nSet-Cookie: admin=true"
// Result: Injects additional Set-Cookie header

Why this is vulnerable: User input containing CRLF sequences (\r\n) can inject additional HTTP headers or split the response into multiple responses, potentially leading to cache poisoning, session fixation, or XSS attacks.

Redirect Without Validation

// VULNERABLE - Redirect without validation
return Redirect(userProvidedUrl);

// Attack example:
// Input: "/home\r\nSet-Cookie: session=hijacked"
// Result: Injects malicious cookie via CRLF in Location header

Why this is vulnerable: Unvalidated redirect URLs can contain CRLF characters that break out of the Location header, allowing attackers to inject arbitrary HTTP headers or response content, enabling session hijacking or phishing attacks.

Content-Type Header Manipulation

// VULNERABLE - User-controlled Content-Type charset
Response.ContentType = "text/html; charset=" + userCharset;

// Attack example:
// Input: "utf-8\r\nSet-Cookie: admin=true"
// Result: Injects Set-Cookie header via Content-Type

Why this is vulnerable: CRLF injection in the Content-Type header can inject additional headers or alter the response. Even though charset seems benign, attackers can use CRLF sequences to break out and inject malicious headers like Set-Cookie or X-XSS-Protection.

Custom API Headers with User Data

// VULNERABLE - Reflecting user data in custom headers
Response.Headers["X-User-Agent"] = Request.Headers["User-Agent"];
Response.Headers["X-Request-Id"] = requestId;  // From user input

// Attack example:
// User-Agent: "Mozilla/5.0\r\nX-Admin: true"
// Result: Injects X-Admin header

Why this is vulnerable: Echoing request headers or user-provided data into response headers without sanitization allows CRLF injection. Even debug/diagnostic headers that seem harmless can become attack vectors if they contain unsanitized user input.

CORS Header Injection

// VULNERABLE - Dynamic CORS header construction
string origin = Request.Headers["Origin"];
Response.Headers["Access-Control-Allow-Origin"] = origin;

// Attack example:
// Origin: "https://trusted.com\r\nAccess-Control-Allow-Credentials: true"
// Result: Injects credentials header

Why this is vulnerable: Dynamically setting CORS headers based on request headers without validation allows CRLF injection. Attackers can inject additional CORS headers or other security-sensitive headers, potentially bypassing same-origin policies.

Secure Patterns

Use Framework Redirect Methods (Best Practice)

// ✅ BEST - Framework handles header construction safely
public IActionResult SafeRedirect(string returnUrl)
{
    // Framework redirect methods handle encoding automatically
    if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);  // Framework encodes Location header
    }

    return RedirectToAction("Index", "Home");
}

Why this works: ASP.NET Core's Redirect() and RedirectToAction() methods construct the Location header internally, automatically handling URL encoding and preventing header injection. The framework validates and encodes the URL before setting the header, making it impossible for user input to inject CRLF characters or additional headers. Url.IsLocalUrl() adds defense-in-depth by preventing open redirects (CWE-601), ensuring the URL is relative to the application. This is the preferred approach because framework methods have battle-tested implementations that handle edge cases like null bytes, Unicode normalization, and encoding bypasses without requiring manual sanitization.

Validate and Sanitize When Manual Headers Required

// ✅ GOOD - Defense-in-depth when framework APIs unavailable
public IActionResult CustomHeaderRedirect(string returnUrl)
{
    // 1. Validate format (prevents open redirect)
    if (!Url.IsLocalUrl(returnUrl))
        return RedirectToAction("Index", "Home");

    // 2. Remove dangerous characters
    returnUrl = returnUrl.Replace("\r", "").Replace("\n", "").Replace("\0", "");

    // 3. Remove control characters
    returnUrl = new string(returnUrl.Where(c => c >= 32 || c == '\t').ToArray());

    // 4. URL encode for additional safety
    string encoded = UrlEncoder.Default.Encode(returnUrl);

    // 5. Use framework API to set header
    Response.Headers["Location"] = encoded;
    Response.StatusCode = 302;
    return new EmptyResult();
}

Why this works: This defense-in-depth approach combines multiple protections when framework redirect methods cannot be used: (1) Url.IsLocalUrl() validates the URL is relative, preventing open redirects (CWE-601). (2) Removing CR (\r), LF (\n), and null bytes (\0) prevents basic header injection. (3) Filtering control characters (ASCII < 32) blocks other injection vectors like tab, form feed, or vertical tabs that might be interpreted by proxies. (4) UrlEncoder.Default.Encode() percent-encodes special characters, preventing any remaining encoded CRLF sequences from being interpreted. (5) Using Response.Headers collection (not AddHeader) ensures proper header handling. This multi-layered approach should only be used when framework redirect methods are truly unavailable.

Use CookieOptions for Setting Cookies

// SECURE - Use CookieOptions for cookie management
Response.Cookies.Append("session", value, new CookieOptions
{
    HttpOnly = true,
    Secure = true,
    SameSite = SameSiteMode.Strict
});

Why this works: ASP.NET Core's CookieOptions API automatically formats cookies correctly and prevents header injection. The framework handles escaping and formatting internally, ensuring that cookie values cannot contain CRLF characters that would break out of the Set-Cookie header. This eliminates the attack surface entirely by never allowing raw header manipulation for cookie values.

URL Encode Header Values

// SECURE - URL encode and sanitize custom header values
string safeParam = UrlEncoder.Default.Encode(userInput.Replace("\r", "").Replace("\n", ""));
Response.Headers.Add("Custom-Header", $"value={safeParam}");

Why this works: UrlEncoder.Default.Encode() converts special characters (including any remaining encoded CRLF sequences like %0d%0a) into percent-encoded format, preventing them from being interpreted as header delimiters. First removing literal \r\n characters ensures that even if URL decoding happens later in the pipeline, there are no raw CRLF characters present. This defense-in-depth approach protects against both direct CRLF injection and encoded variants.

Use ContentDispositionHeaderValue for File Downloads

// SECURE - Use strongly-typed header value for Content-Disposition
var contentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
{
    FileName = filename.Replace("\r", "").Replace("\n", "")
};
Response.Headers.Add("Content-Disposition", contentDisposition.ToString());

Why this works: ContentDispositionHeaderValue is a strongly-typed class that properly formats and escapes the Content-Disposition header according to RFC standards. It automatically handles special characters and quoting, preventing header injection attacks. By removing CRLF characters from the filename first, you ensure that even malicious filenames like file.txt\r\nContent-Type: text/html cannot break out of the header structure. The class's ToString() method generates a properly formatted header value that cannot be used for response splitting.

Validate Content-Type Components

// SECURE - Allowlist validation for Content-Type
private static readonly HashSet<string> AllowedCharsets = new HashSet<string>
{
    "utf-8", "utf-16", "iso-8859-1"
};

public IActionResult ServeContent(string charset)
{
    // Validate against allowlist
    if (!AllowedCharsets.Contains(charset?.ToLower()))
    {
        charset = "utf-8";  // Safe default
    }

    Response.ContentType = $"text/html; charset={charset}";
    return Content(htmlContent);
}

Why this works: Allowlist validation ensures only known-safe charset values are used in Content-Type headers, preventing CRLF injection entirely. By validating against a predefined set of allowed values, attackers cannot inject arbitrary content including CRLF sequences. The fail-safe default ensures invalid input results in a safe, known-good charset value. This approach is superior to sanitization because it prevents both CRLF injection and nonsensical charset values.

Sanitize Custom API Headers

// SECURE - Sanitize before reflecting headers
private string SanitizeHeaderValue(string value)
{
    if (string.IsNullOrEmpty(value))
        return string.Empty;

    // Remove CRLF and null bytes
    value = value.Replace("\r", "").Replace("\n", "").Replace("\0", "");

    // Remove control characters
    value = new string(value.Where(c => c >= 32 || c == '\t').ToArray());

    // Limit length
    if (value.Length > 200)
        value = value.Substring(0, 200);

    return value;
}

public IActionResult ApiEndpoint(string requestId)
{
    Response.Headers["X-Request-Id"] = SanitizeHeaderValue(requestId);
    return Ok();
}

Why this works: Comprehensive sanitization removes CRLF, null bytes, and control characters that could be used for header injection. Length limiting prevents buffer overflow attacks and excessively long header values. This defense-in-depth approach is necessary when custom headers must include user-provided data. The sanitization ensures that even if an attacker tries various encoding tricks, the dangerous characters are removed before the header is set.

Validate CORS Origins with Allowlist

// SECURE - Allowlist-based CORS origin validation
private static readonly HashSet<string> AllowedOrigins = new HashSet<string>
{
    "https://trusted.com",
    "https://app.trusted.com"
};

public IActionResult CorsEndpoint()
{
    string origin = Request.Headers["Origin"];

    if (!string.IsNullOrEmpty(origin) && AllowedOrigins.Contains(origin))
    {
        Response.Headers["Access-Control-Allow-Origin"] = origin;
    }

    return Ok();
}

Why this works: Allowlist validation for CORS origins prevents both CRLF injection and unauthorized cross-origin access. By only setting the Access-Control-Allow-Origin header when the origin exactly matches a trusted value, attackers cannot inject CRLF sequences or bypass same-origin policies. The exact string matching (not substring or pattern matching) prevents bypasses like https://trusted.com.evil.com. This is the secure pattern recommended by OWASP for CORS implementation.

Verification

After implementing CRLF sanitization and framework-based header methods, verify the fix through multiple approaches:

  • Manual testing: Submit HTTP headers with CRLF sequences (\r\n, %0d%0a) and verify they're stripped or rejected
  • Code review: Confirm all header manipulation uses framework APIs (Response.Headers, CookieOptions, ContentDispositionHeaderValue) with no direct string concatenation
  • Static analysis: Use security scanners to verify no response splitting vulnerabilities exist
  • Regression testing: Ensure legitimate header values and redirect functionality continue to work correctly
  • Edge case validation: Test with encoded CRLF (%0d%0a) and verify proper handling
  • Framework verification: Confirm ASP.NET Core's built-in header protections are used correctly
  • HTTP proxy inspection: Use tools like Burp Suite or Fiddler to inspect actual HTTP responses for injected headers or split responses
  • Redirect validation: Test Url.IsLocalUrl() validation prevents open redirects combined with response splitting
  • Rescan: Run the security scanner again to confirm the finding is resolved

Additional Resources