Request Smuggling vs Request Splitting Attack Difference Spring Boot
Both attacks have CRLF in their DNA, both abuse HTTP parsing, and both can let an attacker inject content that the server treats as legitimate. That surface-level similarity is where the resemblance ends. Request smuggling desynchronizes connection state between a reverse proxy and an upstream service, poisoning the request queue. Request splitting injects newlines into a response the application is already building, forging headers or redirects. In Spring Boot deployments they hit completely different layers, have different preconditions, and need different fixes.
How Smuggling and Splitting Actually Work
Request smuggling exploits disagreement about where one HTTP/1.1 request ends and the next begins. A reverse proxy (HAProxy, nginx, an AWS ALB) uses Content-Length to decide how many bytes belong to the first request. The backend Tomcat instance uses Transfer-Encoding: chunked instead, or vice versa. The bytes the proxy thinks are part of the body become a partial second request that Tomcat prepends to the next real user's request. That's the CL.TE variant. The TE.CL variant reverses which side gets fooled.
Request splitting (also called CRLF injection) is an application-layer bug. The application takes a user-controlled value and writes it directly into an HTTP response header or into a forwarded request without stripping carriage-return/line-feed characters. When the client (or an intermediate cache) parses the response, the injected \r\n ends the current header and starts a new one the attacker controls.
For a deep dive on HTTP request smuggling covering gadget chains and cache poisoning variants, the Code Review Lab lesson is worth reading alongside this article.
Here are minimal wire-level examples of each:
# CL.TE smuggling payload (raw TCP, proxy reads Content-Length: 13,
# backend reads Transfer-Encoding and treats the "0\r\n\r\nGET /admin" as a new request)
POST / HTTP/1.1
Host: internal.example.com
Content-Length: 13
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: internal.example.com
// CRLF injection in a Spring MVC controller — splitting the Location header
// Attacker supplies: redirectUrl = "https://safe.example.com\r\nSet-Cookie: session=attacker"
@GetMapping("/redirect")
public void redirect(
@RequestParam String redirectUrl,
HttpServletResponse response) throws IOException {
// No sanitization — injects the attacker's header directly into the response
response.sendRedirect(redirectUrl);
}
When sendRedirect writes the Location header, the embedded \r\n terminates that header line and the content after it lands in the raw response as a new Set-Cookie header. Any browser or CDN that parses the response will treat the injected cookie as authoritative.
The key architectural difference: smuggling requires control over a connection shared between a load balancer and a backend. Splitting requires only the ability to inject a newline into an application-controlled string. You can exploit splitting with nothing but a browser. Smuggling needs raw TCP-level access or, at minimum, a proxy that forwards ambiguous framing.
Fixing Both Attacks in a Spring Boot Service
Fixing CRLF / request splitting
Strip or reject CR and LF before any user-controlled value touches a response header. Do this at the point of use, not only at the controller layer, because the tainted value might travel through a service call before hitting HttpServletResponse.
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@GetMapping("/redirect")
public void safeRedirect(
@RequestParam String redirectUrl,
HttpServletResponse response) throws IOException {
String sanitized = sanitizeHeaderValue(redirectUrl);
// Allowlist check: only redirect to known-safe origins
if (!isAllowedRedirectTarget(sanitized)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid redirect target");
return;
}
response.sendRedirect(sanitized);
}
private String sanitizeHeaderValue(String value) {
if (value == null) return "";
// Strip CR, LF, and null bytes — each can terminate a header line in
// different HTTP implementations
return value.replaceAll("[\\r\\n\\x00]", "");
}
private boolean isAllowedRedirectTarget(String url) {
try {
URI uri = new URI(url);
String host = uri.getHost();
return host != null && host.endsWith(".example.com"); // replace with your domain list
} catch (URISyntaxException e) {
return false;
}
}
Fixing smuggling at the embedded server layer
Tomcat's protection against ambiguous framing has improved significantly since 9.0.31 and 10.0.0-M5. Reject requests that carry both Content-Length and Transfer-Encoding headers, which is what RFC 7230 §3.3.3 requires anyway.
# application.properties
# Reject requests with conflicting framing headers.
# Tomcat 9.0.31+ / 10.x raises an error on ambiguous framing by default,
# but explicitly setting this catches downgrades via dependency drift.
server.tomcat.reject-illegal-header=true
# Disable HTTP/1.1 connection reuse for the reverse proxy channel if you
# are terminating TLS at Tomcat and cannot enforce HTTP/2 end-to-end.
server.tomcat.connection-timeout=20000
If you're behind nginx or HAProxy, enforce normalization there too. On nginx, proxy_http_version 1.1 combined with proxy_set_header Connection "" forces a clean connection model. Switching the proxy-to-backend leg to HTTP/2 eliminates the framing ambiguity entirely because HTTP/2 frames are length-prefixed at the binary level.
Side-by-Side Comparison Table
| Dimension | Request Smuggling | Request Splitting |
|---|---|---|
| Attack surface | Connection shared between proxy and backend | Any response header or forwarded request built from user input |
| Required precondition | Proxy and backend disagree on framing headers | Application writes unsanitized user input to headers |
| Protocol layer | Transport/framing (HTTP/1.1 connection semantics) | Application (header construction) |
| Typical impact | Cache poisoning, request queue desync, access control bypass, reflected XSS against other users | Header injection, forged redirects, session fixation, response splitting |
| Session hijacking path | Capture another user's request by poisoning the backend queue | Inject a Set-Cookie header to fixate a victim's session (see session hijacking via broken authentication) |
| Detection difficulty | High; requires understanding proxy/backend topology | Lower; straightforward grep for unsanitized header writes |
| Affected HTTP versions | HTTP/1.1 primarily; not applicable to HTTP/2 end-to-end | HTTP/1.x and HTTP/2 (header values still carry injection risk) |
| Spring Boot mitigation | Upgrade Tomcat, enforce strict framing, use HTTP/2 | Sanitize input, use allowlists, rely on container-level header encoding |
One thing the table can't show: smuggling is a latent bug that often only becomes exploitable when your infrastructure changes. Adding a CDN or migrating load balancers can turn a harmless misconfiguration into an active attack surface overnight.
Reproducing Each Attack Against a Local Spring Boot App
For testing, you need a setup where a reverse proxy sits in front of a Spring Boot app. A minimal docker-compose with nginx 1.19 (old enough to have permissive framing behavior) in front of a Spring Boot 2.5 app works. The goal here is lab-only validation; never run these against production.
Reproducing TE.CL smuggling
The backend reads Content-Length and treats the overrun as the start of a new request.
# Send raw HTTP over TCP using curl's --http1.1 and --data-binary.
# The chunk size (1) is what the backend counts; the proxy sees Content-Length: 4
# and hands off what it thinks is the full body.
curl -v --http1.1 -s \
-H "Host: localhost" \
-H "Transfer-Encoding: chunked" \
-H "Content-Length: 4" \
--data-binary $'1\r\nZ\r\n0\r\n\r\nGET /admin HTTP/1.1\r\nHost: localhost\r\n\r\n' \
http://localhost:80/api/data
# Follow up with a normal request from a "second user" — the backend may
# prepend the injected GET /admin prefix to it.
curl -v --http1.1 http://localhost:80/api/data
Reproducing CRLF splitting
# Inject a second header into the Location response via a query parameter.
# %0d%0a is URL-encoded CRLF.
curl -v "http://localhost:8080/redirect?redirectUrl=https%3A%2F%2Fsafe.example.com%0d%0aSet-Cookie%3A%20session%3Dattacker_controlled"
# In the response headers you should see:
# Location: https://safe.example.com
# Set-Cookie: session=attacker_controlled
Modern servlet containers (Tomcat 8.5.x+ with CVE-2016-6816 patched) will reject the CRLF in the header value and throw an IllegalArgumentException before the response goes out. If your app swallows that exception and falls back to an unvalidated write, you're still exposed. Test explicitly rather than assuming the container saves you.
Detection in Code Review and CI
The patterns to grep for are tighter than you might expect.
For splitting, the risk is wherever user-controlled data enters a response header. Flag these call sites:
response.addHeader(*, <tainted>)
response.setHeader(*, <tainted>)
response.sendRedirect(<tainted>)
httpHeaders.set(*, <tainted>) // Spring's HttpHeaders
restTemplate.exchange with UriComponentsBuilder using user input
A Semgrep rule that catches the most common Spring variant:
rules:
- id: crlf-injection-spring-response
patterns:
- pattern: |
$RESP.sendRedirect($URL);
- pattern-not: |
$RESP.sendRedirect(sanitize($URL));
- pattern-not: |
$RESP.sendRedirect($CONSTANT);
message: >
sendRedirect called with potentially tainted input — CRLF injection
can forge response headers. Sanitize $URL before use.
languages: [java]
severity: ERROR
metadata:
cwe: "CWE-113"
For smuggling, static analysis is less effective because the bug lives in infrastructure configuration, not application code. Instead, review:
- Any custom
HttpMessageConverteror filter that manually parses or forwards rawContent-Length/Transfer-Encodingheaders. -
RestTemplateorWebClientconfigurations that forward all incoming headers to an upstream service without a header allowlist. This pattern turns your Spring service into a hop that can relay ambiguous framing to an internal backend. - Your
pom.xmlorbuild.gradlefor the exact Tomcat version pulled in transitively. Tomcat 9.0.31+ and 10.0.0-M5+ reject ambiguous framing by default; anything older needs explicit configuration or upgrade.
The application security engineer review playbook on Code Review Lab has a broader checklist for structuring these reviews across a team.
Common Misconceptions Between the Two Attacks
"Both attacks are just CRLF injection." Splitting is CRLF injection. Smuggling uses CRLF only in the sense that HTTP/1.1 header lines are terminated by \r\n. The actual exploitation mechanism for smuggling is the framing disagreement, not literal injection of newlines.
"Tomcat 9+ solves both." Tomcat's strict header parsing largely mitigates splitting (CVE-2016-6816 was patched in 8.5.8). It also rejects ambiguous Content-Length + Transfer-Encoding combos. But if your nginx or HAProxy in front of Tomcat normalizes headers before forwarding them, Tomcat never sees the ambiguity to reject. The vulnerability lives at the proxy layer, not the backend. Upgrading only Tomcat while leaving an old proxy in place fixes nothing for smuggling.
"HTTP/2 kills smuggling completely." HTTP/2 between the client and the proxy, yes. The proxy-to-backend leg is frequently downgraded to HTTP/1.1, and that's where smuggling happens. The only complete fix is HTTP/2 end-to-end with no HTTP/1.1 hop between any pair of components.
"Splitting is dead." Modern containers reject CRLF in sendRedirect and setHeader. But Spring's HttpHeaders.set(), RestTemplate forwarding, and custom header-building code in filters do not always go through the same validation path. We hit this in production with a filter that read an X-Forwarded-Host value and echoed it into an upstream request via RestTemplate without sanitization. The container never touched it. See also related parsing-ambiguity issues like HTTP parameter pollution for similar trust-boundary failures in header forwarding code.
"These only affect public-facing services." Smuggling against internal services behind a shared proxy can be more damaging because internal services often run with lower authentication requirements. A poisoned request queue against an admin API is a much bigger blast radius than against a public endpoint.
Checklist for Spring Boot Teams
Run through this when onboarding a new service or auditing an existing one:
-
Embedded server version. Confirm Tomcat is at 9.0.31+ (Spring Boot 2.x) or 10.1+ (Spring Boot 3.x). Check
mvn dependency:tree | grep tomcat-embed-core. Do not assume the Spring Boot BOM is always current if you've pinned parent versions. -
Reject ambiguous framing. Set
server.tomcat.reject-illegal-header=trueand verify it's active by sending a request with bothContent-LengthandTransfer-Encodingheaders to a non-production environment and confirming a 400 response. -
Proxy configuration audit. Review nginx/HAProxy/ALB settings for
proxy_http_version, header normalization, and whether the proxy-to-backend leg supports HTTP/2. - HTTP/2 end-to-end. Where operationally feasible, terminate HTTP/2 all the way to Tomcat. This removes the framing ambiguity vector at the protocol level.
-
Header value sanitization. Add a shared utility that strips
\r,\n, and\x00from any string that will become a header value. Enforce its use via Semgrep in CI, not just in code review. - Allowlist redirect targets. Never use a user-supplied URL as a redirect target without validating it against an allowlist of known origins. Sanitizing the CRLF is a layer of defense; restricting the destination is the primary control.
-
Header forwarding review. Audit any
RestTemplate,WebClient, orHttpClientusage that forwards incoming request headers upstream. Maintain an explicit allowlist of headers to forward; reject or drop everything else. -
Regression tests. Add integration tests that send
%0d%0ain redirect parameters and confirm a 400 response. Add a test that sends bothContent-LengthandTransfer-Encodingand confirms rejection. These tests are cheap and catch library upgrade regressions before production does.
The combination of a patched embedded server, a well-configured proxy, and disciplined header sanitization in application code covers both attack classes. No single control is sufficient on its own; they work as a stack.
If you ship a new Spring Boot service tomorrow, the highest-leverage single action is checking the Tomcat version and adding the CRLF-stripping sanitizer to your shared utilities library before any controller touches user-controlled redirect URLs. Fixing the application code is faster than coordinating a proxy config change, and it's the layer you actually own.
Top comments (0)