Post Mortem: HTTP Request Smuggling Vulnerability
On 12 April 2026, we received a vulnerability report
regarding an HTTP request smuggling vulnerability in HTTP::Server.
The issue was caused by the HTTP request parser prioritizing the
Content-Length header over the Transfer-Encoding header, which can lead to
desynchronization when a proxy that prioritizes the Transfer-Encoding header
sits in front of HTTP::Server for example.
The vulnerability was patched in Crystal 1.20.0 and Crystal 1.19.2,
following the mitigation from RFC 9112, Section 6.1 to always reject requests
with both headers and to prioritize Transfer-Encoding then close the
connection.
Timeline
Section titled Timeline-
2026-04-12: Gabriel Rodrigues reported the vulnerability to security@manas.tech
-
2026-04-13: The report was shared with the Crystal Core Team and acknowledged to the reporter. The risk was assessed as low by team.
-
2026-04-15: The patch of choice (rejecting requests with both headers) was confirmed as the mitigation. The reporter was informed about the assessment and mitigation strategy.
-
2026-04-16: The patch was merged and a security advisory was published. Crystal 1.20.0 was released with the fix.
-
2026-04-27: Crystal 1.19.2 was released with the backported patch.
Technical Details
Section titled Technical DetailsRoot Cause
Section titled Root CauseThe issue resided in the HTTP Request parser, specifically in the
parse_headers_and_body method. The method used elsif to
connect the checks for Content-Length and Transfer-Encoding, ensuring only
one branch would execute. This meant that if Content-Length was present, the
Transfer-Encoding branch would never be evaluated, regardless of its value.
elsif content_length = content_length(headers)
body = FixedLengthContent.new(io, content_length)
elsif headers["Transfer-Encoding"]? == "chunked"
body = ChunkedContent.new(io)
This behavior wasn’t compliant with the mitigation from RFC 9112, Section 6.1:
A server MAY reject a request that contains both
Content-LengthandTransfer-Encodingor process such a request in accordance with theTransfer-Encodingalone. Regardless, the server MUST close the connection after responding to such a request to avoid the potential attacks.A server or client that receives an HTTP/1.0 message containing a
Transfer-Encodingheader field MUST treat the message as if the framing is faulty, even if aContent-Lengthis present, and close the connection after processing the message.
Example
Section titled ExampleAn attacker could craft a request like the following (no proxy required for demonstration):
POST / HTTP/1.1
Host: example.com
Content-Length: 4
Transfer-Encoding: chunked
43
GET /admin HTTP/1.1
Host: example.com
0
This request is ambiguous: Content-Length declares a 4 bytes body (43\r\n)
while Transfer-Encoding would start reading. Since HTTP::Server favors
Content-Length it would execute the POST / request then continue with the
smuggled GET /admin request.
Impact
Section titled ImpactThe vulnerability allowed attackers to inject arbitrary HTTP requests into the
connection between a reverse proxy and a Crystal HTTP::Server. This could
bypass authentication, rate limiting, or access control enforced at the proxy
layer for example. However, exploitation required a vulnerable or non-compliant
proxy, limiting the practical impact.
Severity Assessment
Section titled Severity Assessment- Can it be directly exploited? No.
- Can it be exploited through a third party? Yes: vulnerable proxy or load balancer.
- Are there possible exploits for the software flaw? Yes: CL.TE for example.
- Are there known exploits for HTTP::Server + third party? No.
- Are there known threats? No.
Risk: low.
Mitigation
Section titled MitigationThe vulnerability stemmed from non-compliance with RFC 9112, which explicitly
states that a server should either reject requests with both headers or
prioritize Transfer-Encoding to mitigate request smuggling.
HTTP::Server thus now rejects requests with both Content-Length and
Transfer-Encoding and the HTTP parser now prioritizes the Transfer-Encoding
header before considering Content-Length, aligning with the RFC 9112.
This fix was included in Crystal 1.20.0 and 1.19.2.
Lessons Learned
Section titled Lessons Learned-
RFC Compliance Matters: Adhering to RFCs is critical for security and interoperability. Non-compliance can introduce vulnerabilities, even if they seem theoretical.
-
Vulnerability in Chains: A vulnerable server may not be at risk, but a bug and a vulnerability in different software can lead to an exploitable attack.
-
Training: We treated this as a serious issue, even if the practical impact was low, if only to prepare for more severe vulnerabilities in the future.
References
Section titled References- GitHub Advisory: GHSA-wqh5-7w63-pm68
- RFC 9112, Section 6.1
- PortSwigger: HTTP Request Smuggling
- PortSwigger: HTTP/1 Must Die
Acknowledgements
Section titled AcknowledgementsThank you Gabriel Rodrigues for reporting this vulnerability!