CWE-113: HTTP Response Splitting - Java
Overview
HTTP Response Splitting occurs when attackers inject CRLF characters into HTTP headers, potentially allowing them to inject additional headers or response bodies.
Primary Defence: Use Spring Framework's redirect methods (redirect: prefix in @Controller return values, RedirectView), ResponseCookie.from() builder, and ContentDisposition.builder() which automatically handle encoding and prevent header injection. When manual header manipulation is unavoidable, apply defense-in-depth: validate input format (relative URLs for Location headers), remove CRLF characters (.replaceAll("[\\r\\n\\0]", "")), filter control characters, and URL-encode values with UriComponentsBuilder or URLEncoder. Prefer framework APIs over manual sanitization whenever possible.
Common Vulnerable Patterns
Direct Header Injection
// VULNERABLE - Direct header injection
@GetMapping("/redirect")
public void redirect(@RequestParam String url, HttpServletResponse response) {
response.setHeader("Location", url); // Vulnerable!
}
// Attack example:
// Input: "/home\r\nSet-Cookie: admin=true"
// Result: Injects additional Set-Cookie header
Why this is vulnerable: Unsanitized user input in HTTP headers can contain CRLF (\r\n) characters that break out of the intended header, allowing injection of malicious headers or complete HTTP responses, leading to session fixation, cache poisoning, or XSS.
Cookie Manipulation via String Concatenation
// VULNERABLE - Cookie manipulation
response.setHeader("Set-Cookie", "session=" + sessionId);
// Attack example:
// Input: "abc123\r\nSet-Cookie: admin=true; HttpOnly"
// Result: Injects malicious cookie header
Why this is vulnerable: Concatenating user-controlled data into Set-Cookie headers without CRLF sanitization allows attackers to inject additional cookies or headers, potentially leading to privilege escalation or session hijacking.
Content-Type Header Manipulation
// VULNERABLE - User-controlled Content-Type charset
@GetMapping("/content")
public void serveContent(@RequestParam String charset, HttpServletResponse response) {
response.setContentType("text/html; charset=" + charset);
}
// Attack example:
// Input: "utf-8\r\nX-XSS-Protection: 0"
// Result: Disables XSS protection via Content-Type injection
Why this is vulnerable: CRLF injection in Content-Type allows attackers to inject additional headers. Even though charset appears harmless, CRLF sequences can break out of the Content-Type header to inject security-sensitive headers or alter response behavior.
Custom API Headers with User Data
// VULNERABLE - Reflecting request headers
@GetMapping("/api/data")
public void getData(HttpServletRequest request, HttpServletResponse response) {
String userAgent = request.getHeader("User-Agent");
response.setHeader("X-User-Agent", userAgent); // Dangerous!
}
// Attack example:
// User-Agent: "Bot\r\nX-Admin: true"
// Result: Injects X-Admin header
Why this is vulnerable: Echoing request headers into response headers without sanitization allows CRLF injection. Diagnostic or logging headers that seem benign become attack vectors when they contain unsanitized user input.
CORS Header Injection
// VULNERABLE - Dynamic CORS without validation
@GetMapping("/api/resource")
public void corsResource(HttpServletRequest request, HttpServletResponse response) {
String origin = request.getHeader("Origin");
response.setHeader("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: Setting CORS headers based on request headers without validation allows CRLF injection and bypasses same-origin policies. Attackers can inject additional CORS headers or other security-critical headers.
Secure Patterns
Use Spring Framework Redirect (Best Practice)
// BEST - Framework handles header construction safely
@GetMapping("/redirect")
public String redirect(@RequestParam String returnUrl) {
// Validate URL is local (prevents open redirect)
if (returnUrl == null || !returnUrl.startsWith("/") || returnUrl.startsWith("//")) {
return "redirect:/";
}
// Spring's redirect: prefix handles Location header encoding
return "redirect:" + returnUrl;
}
Why this works: Spring Framework's redirect: prefix constructs the Location header internally, automatically URL-encoding the path and preventing header injection. The framework validates and encodes the URL before setting the HTTP header, making it impossible for user input to inject CRLF characters or additional headers even if they try encoded variants like %0d%0a. The validation that returnUrl starts with / but not // ensures it's a relative path within the application, preventing open redirects (CWE-601). This is the preferred approach because Spring handles all edge cases including null bytes, Unicode normalization, and various encoding bypasses without requiring manual sanitization.
Defense-in-Depth When Manual Headers Required
// GOOD - Multi-layered protection when framework APIs unavailable
@GetMapping("/custom")
public ResponseEntity<Void> customRedirect(@RequestParam String returnUrl,
HttpServletResponse response) {
// 1. Validate URL format
if (returnUrl == null || !returnUrl.startsWith("/") ||
returnUrl.startsWith("//") || returnUrl.contains(":")) {
return ResponseEntity.status(302).header("Location", "/").build();
}
// 2. Remove CRLF and null bytes
returnUrl = returnUrl.replaceAll("[\\r\\n\\0]", "");
// 3. Remove control characters (ASCII < 32)
returnUrl = returnUrl.replaceAll("[\\x00-\\x1F]", "");
// 4. URL encode for additional safety
String encoded = UriComponentsBuilder.fromPath(returnUrl)
.build()
.toUriString();
return ResponseEntity.status(302).header("Location", encoded).build();
}
Why this works: This defense-in-depth approach combines multiple protections when Spring's redirect methods cannot be used: (1) Validates URL is a relative path, blocking external URLs and protocol-relative URLs to prevent open redirects. (2) Removes CR, LF, and null bytes that could be used for header injection. (3) Filters all control characters (ASCII 0-31) to block tab, form feed, vertical tab, and other special characters. (4) UriComponentsBuilder properly encodes the path according to RFC 3986, preventing any encoded CRLF sequences from being interpreted. This multi-layered approach should only be used when framework redirect methods are unavailable.
Use Spring ContentDisposition Builder
// SECURE - Use Spring ContentDisposition builder for file downloads
@GetMapping("/download")
public ResponseEntity<byte[]> download(@RequestParam String filename) {
String safeName = filename.replaceAll("[\\r\\n]", "");
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(
ContentDisposition.attachment().filename(safeName).build()
);
return new ResponseEntity<>(data, headers, HttpStatus.OK);
}
Why this works: Spring's ContentDisposition builder automatically formats the Content-Disposition header according to RFC 6266, properly escaping special characters and quoting the filename. By removing CRLF characters first with replaceAll("[\\r\\n]", ""), you prevent attackers from injecting malicious filenames like file.txt\r\nContent-Type: text/html that could break out of the header structure. The builder's toString() method generates a properly formatted header value that cannot be exploited for response splitting.
Use ResponseCookie Builder for Cookies
// SECURE - Use ResponseCookie builder for secure cookie handling
import org.springframework.http.ResponseCookie;
ResponseCookie cookie = ResponseCookie.from("session", value)
.httpOnly(true)
.secure(true)
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
Why this works: Spring's ResponseCookie builder provides a type-safe API that automatically formats cookies correctly according to RFC 6265. The builder internally handles escaping and prevents header injection by ensuring cookie values cannot contain CRLF sequences. The toString() method generates a properly formatted Set-Cookie header with all attributes correctly positioned, eliminating the risk of header splitting. This approach is far safer than manual string concatenation because the framework handles all edge cases and encoding requirements.
Validate Content-Type with Allowlist
// SECURE - Allowlist validation for charset
private static final Set<String> ALLOWED_CHARSETS = Set.of(
"utf-8", "utf-16", "iso-8859-1"
);
@GetMapping("/content")
public void serveContent(@RequestParam String charset, HttpServletResponse response) {
// Validate against allowlist
if (charset == null || !ALLOWED_CHARSETS.contains(charset.toLowerCase())) {
charset = "utf-8"; // Safe default
}
response.setContentType("text/html; charset=" + charset);
}
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 charset. This approach is superior to sanitization because it prevents both CRLF injection and nonsensical charset values.
Sanitize Custom Headers
// SECURE - Sanitize before reflecting headers
private String sanitizeHeaderValue(String value) {
if (value == null || value.isEmpty()) {
return "";
}
// Remove CRLF and null bytes
value = value.replaceAll("[\\r\\n\\0]", "");
// Remove control characters (ASCII < 32)
value = value.replaceAll("[\\x00-\\x1F]", "");
// Limit length
if (value.length() > 200) {
value = value.substring(0, 200);
}
return value;
}
@GetMapping("/api/data")
public void getData(@RequestParam String requestId, HttpServletResponse response) {
response.setHeader("X-Request-Id", sanitizeHeaderValue(requestId));
}
Why this works: Comprehensive sanitization removes CRLF, null bytes, and all control characters (ASCII 0-31) that could be used for header injection. Length limiting prevents excessively long header values. This defense-in-depth approach is necessary when custom headers must include user-provided data. The multi-step sanitization ensures dangerous characters are removed before the header is set.
Validate CORS Origins with Allowlist
// SECURE - Allowlist-based CORS validation
private static final Set<String> ALLOWED_ORIGINS = Set.of(
"https://trusted.com",
"https://app.trusted.com"
);
@GetMapping("/api/resource")
public void corsResource(HttpServletRequest request, HttpServletResponse response) {
String origin = request.getHeader("Origin");
if (origin != null && ALLOWED_ORIGINS.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
}
Why this works: Allowlist validation for CORS origins prevents both CRLF injection and unauthorized cross-origin access. By only setting Access-Control-Allow-Origin when the origin exactly matches a trusted value, attackers cannot inject CRLF sequences or bypass same-origin policies. The exact string matching (using Set.contains()) prevents bypasses like https://trusted.com.evil.com. This is the secure CORS pattern recommended by OWASP.
Use UriComponentsBuilder for URL Parameters
// SECURE - Use UriComponentsBuilder for URL construction
import org.springframework.web.util.UriComponentsBuilder;
String url = UriComponentsBuilder.fromPath("/page")
.queryParam("returnUrl", userInput)
.build()
.toUriString();
response.setHeader("Location", url);
Why this works: UriComponentsBuilder automatically URL-encodes query parameters, converting special characters (including encoded CRLF sequences like %0d%0a) into safe percent-encoded format. Each component of the URI is encoded according to RFC 3986 standards, ensuring that characters with special meaning in HTTP headers cannot break out of the Location header value. The builder pattern also prevents common mistakes in manual URL construction, making the code more maintainable and secure by design.