Request smuggling vs request splitting in Spring Boot: what to check for each
Contents
TL;DR
The distinction Request smuggling is a framing mismatch between your proxy and Tomcat. Request splitting is CRLF reaching controllers or filters. You check different places for each.
To check smuggling The real tomcat-embed-core version and the front proxy’s HTTP/1.1 forwarding config. Don’t trust a release line as “safe” — track the latest patch level.
To check splitting Grep for sendRedirect, setHeader, RestTemplate, WebClient. Enforce a redirect-target allowlist and reject CRLF in header values.
If you treat request smuggling and request splitting in a Spring Boot app as the same CRLF-family problem, you check the wrong places.
Stefan’s article splits the two apart specifically for the Spring Boot + Tomcat setup.
Request smuggling is when the front proxy and the backend Tomcat read the same HTTP/1.1 byte stream with different boundaries.
When the handling of Content-Length and Transfer-Encoding diverges, the bytes the proxy thinks are a body get processed by the backend as the next request.
Request splitting is when CRLF (carriage return + line feed, \r\n, the control characters HTTP uses to delimit lines) gets into a header value or redirect target that the application builds, splitting the response headers or the forwarded request. OWASP’s formal name, “HTTP Response Splitting,” refers to the response side; in addition to that, this article also calls CRLF injection into a forwarded request “request splitting.”
HttpServletResponse#sendRedirect, setHeader, Spring’s HttpHeaders, and header forwarding via RestTemplate or WebClient are all in scope.
Because the names are similar and both involve CRLF and HTTP headers, it’s easy to mix up the check procedures. Contrast them on three points — the layer where they occur, what you check, and the mitigation — and it’s clear they’re separate jobs.
| Aspect | Request Smuggling | Request Splitting |
|---|---|---|
| Layer where it occurs | HTTP/1.1 message boundary between proxy and Tomcat | String handling of header values / redirect targets in the app |
| Cause | Front and backend interpret framing differently | CRLF in untrusted input passes as a header terminator |
| What to check | tomcat-embed-core version, front proxy forwarding config | sendRedirect, setHeader, HTTP client forwarding code |
| Main mitigation | Reject ambiguous requests at the proxy, carry the hop over HTTP/2 | Redirect-target allowlist, reject CRLF in header values |
| Main touch points in Spring Boot | Dependency tree and front proxy config | Controllers, filters, client calls |
Diagramming where the framing difference is born makes it clear that smuggling happens in the interpretation gap between two servers.
flowchart TD
A[クライアント] -->|CLとTEを同居させた<br/>HTTP/1.1リクエスト| B[フロントプロキシ<br/>nginx HAProxy ALB]
B -->|片方のヘッダで<br/>境界を決めて転送| C[バックエンド<br/>組み込みTomcat]
C -->|別のヘッダで<br/>境界を決めて解釈| D[境界がずれる]
D --> E[残りバイト列が<br/>次リクエストの先頭になる]
E --> F[後続ユーザーの<br/>リクエストへ前置注入]
Smuggling happens at the proxy–Tomcat boundary
Spring Boot’s Servlet stack uses the embedded Tomcat by default.
Spring Boot’s official docs also state that spring-boot-starter-web includes Tomcat.
Testing only requests that hit Tomcat alone won’t tell you whether request smuggling is possible.
The attack works when a front layer like nginx, HAProxy, or AWS ALB and the Tomcat side don’t agree on the HTTP/1.1 message boundary.
The variants are classified by which header the front and backend each use to decide the message boundary.
HTTP/1.1 has two ways to determine body length: specifying a byte count with Content-Length, or laying out chunks with Transfer-Encoding: chunked.
When both appear in one request and the front and backend interpret them separately, the boundary diverges.
| Variant | Front-side interpretation | Backend-side interpretation | The divergence that makes it work |
|---|---|---|---|
| CL.TE | Reads body length from Content-Length | Reads it as Transfer-Encoding: chunked | Front prefers Content-Length, backend prefers Transfer-Encoding |
| TE.CL | Reads it as Transfer-Encoding: chunked | Reads body length from Content-Length | Front prefers Transfer-Encoding, backend prefers Content-Length |
| TE.TE | Both read Transfer-Encoding, but one is made to ignore it | Same (the side that ignores it is reversed) | A malformed form (Transfer-Encoding: xchunked etc.) makes only one side fail to parse |
CL.TE and TE.CL are cases where the front and backend prefer the opposite header outright.
TE.TE is where both look at Transfer-Encoding, but a malformed notation like Transfer-Encoding:[Tab]chunked makes only one of them fail to parse, effectively producing a CL.TE- or TE.CL-equivalent boundary divergence.
RFC 7230 Section 3.3.3 (now superseded by RFC 9112 Section 6.1) treats a message with both Transfer-Encoding and Content-Length as a sign of request smuggling or response splitting, and says Content-Length should be removed before forwarding.
The emphasis of the mitigation is not on deciding “which header to prefer,” but on not passing ambiguous framing downstream.
In the Tomcat 9 line, request-smuggling-related fixes landed in 9.0.31 (9.0.30 and earlier are affected).
Per Tomcat’s security page, the range 9.0.0.M1 to 9.0.30 has an issue where invalid HTTP headers are interpreted as valid (CVE-2020-1935), which can lead to request smuggling behind certain reverse proxies.
A regression in 9.0.28 to 9.0.30 (CVE-2019-17569) also produced the same class of issue from invalid Transfer-Encoding header handling.
Both are fixed in 9.0.31.
What you check at this stage is not the controller.
You go through the tomcat-embed-core actually pulled in by pom.xml or build.gradle, the front proxy’s HTTP/1.1 forwarding config, keep-alive, header normalization, and whether the connection to the backend is carried over HTTP/2.
Splitting happens in the string handling of controllers and filters
Request splitting sits at a different layer.
When attacker input ends up in a header value, the CRLF is interpreted as the end of a header line.
OWASP’s HTTP Response Splitting page explains that when untrusted input enters HTTP response headers without validation, an attacker can manipulate the rest of the headers and body.
The CRLF Injection page likewise notes that because HTTP uses CRLF as a line terminator, it leads to issues like HTTP response splitting.
In Spring MVC, it tends to show up in redirect handling.
@GetMapping("/redirect")
public void redirect(
@RequestParam String redirectUrl,
HttpServletResponse response
) throws IOException {
response.sendRedirect(redirectUrl);
}
If redirectUrl gets a value like %0d%0aSet-Cookie:%20session=attacker and it passes through as a header value, an attacker-specified Set-Cookie lines up right after Location.
Recent Servlet containers have a path that rejects header values containing CRLF.
But not every dangerous string passes through the container’s validation.
In code that reads X-Forwarded-Host in a filter and forwards it as-is to an internal API via RestTemplate, the ignition point is the header construction of the “next request to send,” not the response headers.
This is close to the axios prototype pollution gadget and CRLF header injection I wrote about earlier.
That article was about a Node.js HTTP client putting a tainted value into a header. What we’re looking at on the Spring Boot side this time is whether Java controllers, filters, and HTTP client forwarding code pass CRLF into a header value.
A downgrade hop leaves the HTTP/1.1 divergence in place
HTTP/2 uses binary framing, so the HTTP/1.1 mismatch between Content-Length and Transfer-Encoding doesn’t surface directly.
If you can carry the connection from proxy to Tomcat over HTTP/2, the classic CL.TE and TE.CL paths disappear.
That said, a setup where the client-to-proxy hop is HTTP/2 and the proxy-to-backend hop is HTTP/1.1 is common.
PortSwigger’s HTTP/2 request splitting lab covers a setup where the front downgrades HTTP/2 to HTTP/1.1.
If headers aren’t sanitized enough, CRLF splits the request.
Making only the Spring Boot side HTTP/2-capable doesn’t help if there’s a hop along the way that drops back to HTTP/1.1 — the framing divergence remains.
Even within HTTP/2, this is separate from what I covered in the HTTP/2 Bomb article.
That one was a DoS that pins server memory via HPACK, cookie crumbs, and flow control.
This smuggling/splitting issue isn’t about header-compression amplification — it happens because the proxy and backend cut the request or response boundary at different positions.
Files and settings to go through in Spring Boot
As of 2026, the current line is Spring Boot 4, whose default embedded Tomcat is the 11 line.
But if you’re on an older Spring Boot 2/3 line, even when you leave it to the BOM, the actual Tomcat can stay pinned to an old version.
Use mvn dependency:tree or gradle dependencies to print the real tomcat-embed-core version.
mvn dependency:tree | grep tomcat-embed-core
./gradlew dependencies --configuration runtimeClasspath | grep tomcat-embed-core
Don’t judge by the release line here — “9.0.31 or later so it’s safe,” “the 10.1 line so the known fix is in.”
Request smuggling keeps getting found afterward; in 2026, CVE-2026-24880 in chunk extension handling affects a wide range across the 9.0/10.1/11.0 lines (up to 9.0.115, 10.1.52, 11.0.18).
Whether it’s safe can only be stated at the minor version (patch level), so track the real version to the latest rather than the line.
At the same time, check that in a setup where the front proxy normalizes ambiguous requests before forwarding them to the backend, the original anomaly doesn’t reach the Tomcat side.
Reject at the proxy, make the hop to the backend HTTP/2, and verify in a test environment whether co-occurrence of Content-Length and Transfer-Encoding can be made to return 400.
Whether malformed headers are rejected is governed by the Tomcat connector’s rejectIllegalHeader attribute.
In older Tomcat (the 8.5 line) the default was false, so an invalid Content-Length passed through without being rejected. CVE-2022-42252 is the issue where, with this set to false, smuggling works behind a reverse proxy; in newer Tomcat the default has changed to true. With true, a request containing a malformed header is dropped with 400.
On the Spring Boot side, the server.tomcat.reject-illegal-header property was deprecated in 2.7.12 and removed in 3.4 (it doesn’t exist in the Boot 4 line). Check your config files to make sure an old version isn’t explicitly disabling it by setting this to false.
The header length cap is server.max-http-request-header-size (Spring Boot 3 line; the 2 line uses server.max-http-header-size) — look at this alongside a separate class of issue caused by oversized headers.
On the splitting side, start from a code search.
rg "sendRedirect|setHeader|addHeader|HttpHeaders|RestTemplate|WebClient" src
Narrow redirect targets with a domain allowlist.
For strings used in header values, reject \r, \n, and \x00.
Code that forwards received headers upstream should copy only the explicitly named headers it needs, not pass everything through.
References
- Request Smuggling vs Request Splitting in Spring Boot
- Apache Tomcat 9 vulnerabilities
- Apache Tomcat 10.1 HTTP Connector (rejectIllegalHeader)
- Spring Boot Embedded Web Servers
- RFC 7230 Section 3.3.3
- RFC 9112 Section 6.1
- OWASP HTTP Response Splitting
- OWASP CRLF Injection
- PortSwigger HTTP/2 request splitting via CRLF injection