Http4s vulnerable to HTTP Request Smuggling due to improper handling of HTTP trailer section
Description
Http4s is a Scala interface for HTTP services. In versions from 1.0.0-M1 to before 1.0.0-M45 and before 0.23.31, http4s is vulnerable to HTTP Request Smuggling due to improper handling of HTTP trailer section. This vulnerability could enable attackers to bypass front-end servers security controls, launch targeted attacks against active users, and poison web caches. A pre-requisite for exploitation involves the web application being deployed behind a reverse-proxy that forwards trailer headers. This issue has been patched in versions 1.0.0-M45 and 0.23.31.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Http4s HTTP request smuggling vulnerability via improper trailer parsing allows attackers to bypass security controls, poison caches, and target users when deployed behind a reverse-proxy.
Vulnerability
Description
Http4s, a Scala HTTP interface, is affected by an HTTP request smuggling vulnerability arising from improper handling of the HTTP trailer section during chunked transfer encoding [1]. The issue resides in the parseTrailers method of the chunked encoding parser, which calls Parser.parse to process trailer headers. A bug in parse allows parsing to terminate prematurely upon encountering a header line without a colon, bypassing the double CRLF termination condition [3]. This affects versions from 1.0.0-M1 to before 1.0.0-M45 and all versions before 0.23.31 [1].
Exploitation
Prerequisites
Exploitation requires the application to be deployed behind a reverse-proxy that forwards trailer headers [1]. An attacker can send a specially crafted HTTP request with a malformed trailer section that exploits the premature termination, causing the parser to treat part of the request body as the beginning of the next request [3].
Impact
Successful exploitation can allow an attacker to bypass front-end security controls, launch targeted attacks against active users, and poison web caches [3]. This could lead to session hijacking, cache poisoning, and other HTTP desynchronization attacks.
Mitigation
The vulnerability has been patched in http4s versions 1.0.0-M45 and 0.23.31 [1]. The fix involves stricter validation of header fields, rejecting invalid whitespace characters around field names as seen in the referenced commit [2]. Users are advised to upgrade to the patched versions immediately.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.http4s:http4s-ember-core_2.12Maven | < 0.23.31 | 0.23.31 |
org.http4s:http4s-ember-core_2.13Maven | < 0.23.31 | 0.23.31 |
org.http4s:http4s-ember-core_3Maven | < 0.23.31 | 0.23.31 |
org.http4s:http4s-ember-core_2.13Maven | >= 1.0.0-M1, < 1.0.0-M45 | 1.0.0-M45 |
org.http4s:http4s-ember-core_3Maven | >= 1.0.0-M1, < 1.0.0-M45 | 1.0.0-M45 |
Affected products
2- http4s/http4sv5Range: < 0.23.31
Patches
1dd518f7c967efail parsing invalid whitespace around field name
3 files changed · +95 −15
ember-core/shared/src/main/scala/org/http4s/ember/core/Parser.scala+15 −15 modified@@ -27,6 +27,7 @@ import scodec.bits.ByteVector import scala.annotation.switch import scala.util.control.NonFatal +import scala.util.control.NoStackTrace private[ember] object Parser { @@ -116,21 +117,17 @@ private[ember] object Parser { while (!complete && idx <= upperBound) { if (!state) { - val current = message(idx) - // if current index is colon our name is complete - if (current == colon) { - state = true // set state to check for header value - name = new String(message, start, idx - start) // extract name string - start = idx + 1 // advance past colon for next start - - // TODO: This if clause may not be necessary since the header value parser trims - if (message.size > idx + 1 && message(idx + 1) == space) { - start += 1 // if colon is followed by space advance again - idx += 1 // double advance index here to skip the space - } - // double CRLF condition - Termination of headers - } else if (current == lf && (idx > 0 && message(idx - 1) == cr)) { - complete = true // completed terminate loop + (message(idx): @switch) match { + case ':' => + state = true // set state to check for header value + name = new String(message, start, idx - start) // extract name string + start = idx + 1 // advance past colon for next start + case '\r' if start == idx => // proceed + case '\n' if idx > 0 && message(idx - 1) == cr => complete = true + case c if c <= 0x20 | c == 0x7f => + throwable = InvalidHeaderWhitespace + complete = true + case _ => // proceed } } else { val current = message(idx) @@ -182,6 +179,9 @@ private[ember] object Parser { } } + case object InvalidHeaderWhitespace extends Exception with NoStackTrace { + override val getMessage = "InvalidHeaderWhitespace" + } final case class ParseHeadersError(cause: Throwable) extends Exception( s"Encountered Error Attempting to Parse Headers - ${cause.getMessage}",
ember-core/shared/src/test/scala/org/http4s/ember/core/ParserSuite.scala+73 −0 modified@@ -23,6 +23,7 @@ import cats.syntax.all._ import fs2._ import org.http4s._ import org.http4s.headers.Expires +import org.http4s.ember.core.Parser.HeaderP.ParseHeadersError import org.http4s.implicits._ import org.typelevel.ci._ import scodec.bits.ByteVector @@ -254,6 +255,78 @@ class ParsingSuite extends Http4sSuite { } yield req1._1.method == Method.GET && req2._1.method == Method.GET).assert } + test("Parser.Request.parser should fail on invalid whitespace") { + val defaultMaxHeaderLength = 4096 + val reqS = Stream( + "POST /foo HTTP/1.1\r\na\r\n" + ) + val byteStream: Stream[IO, Byte] = reqS.flatMap(s => + Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.US_ASCII))) + ) + + for { + take <- Helpers.taking[IO, Byte](byteStream) + _ <- interceptMessageIO[ParseHeadersError]( + "Encountered Error Attempting to Parse Headers - InvalidHeaderWhitespace" + ) { + Parser.Request.parser[IO](defaultMaxHeaderLength)(Array.emptyByteArray, take) + } + } yield () + } + + test("Parser.Request.parser should handle correct whitespace split across chunks") { + val defaultMaxHeaderLength = 4096 + val raw1 = "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n" + val raw2 = "\r\n2\r\naa\r\n\r\n0\r\nTrailer: header\r" + val raw3 = "\n\r\n" + + val byteStream: Stream[IO, Byte] = + (Stream(raw1) ++ Stream(raw2) ++ Stream(raw3)).through(fs2.text.utf8.encode) + + for { + take <- Helpers.taking[IO, Byte](byteStream) + req <- Parser.Request.parser[IO](defaultMaxHeaderLength)(Array.emptyByteArray, take) + body <- req._1.body.through(text.utf8.decode).compile.string + th <- req._1.trailerHeaders + } yield { + assertEquals(req._1.method, Method.POST) + assertEquals(body, "aa") + assertEquals(th.headers, List(Header.Raw(ci"Trailer", "header"))) + } + } + + test( + "Parser.Request.parser should fail to parse a chunked body with malformed trailer headers" + ) { + val defaultMaxHeaderLength = 4096 + val reqS = + Stream( + "POST /foo HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n", + "\r\n", + "2\r\n", + "aa\r\n", + "\r\n", + "0\r\nTrailer: header\r\n", + "a\r\n", + ) + + val byteStream: Stream[IO, Byte] = reqS + .flatMap(s => + Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.US_ASCII))) + ) + + for { + take <- Helpers.taking[IO, Byte](byteStream) + req1 <- Parser.Request.parser[IO](defaultMaxHeaderLength)(Array.emptyByteArray, take) + _ <- interceptMessageIO[ParseHeadersError]( + "Encountered Error Attempting to Parse Headers - InvalidHeaderWhitespace" + ) { + // drain the body to trigger the trailing header parsing, which will raise an exception + req1._1.body.through(text.utf8.decode).compile.string + } + } yield () + } + test("Parser.Response.parser should handle a chunked response") { val defaultMaxHeaderLength = 4096 val base =
ember-server/shared/src/main/scala/org/http4s/ember/server/internal/ServerHelpers.scala+7 −0 modified@@ -55,6 +55,9 @@ private[server] object ServerHelpers extends ServerHelpersPlatform { private val serverFailure = Response(Status.InternalServerError).putHeaders(org.http4s.headers.`Content-Length`.zero) + private val badRequest = + Response(Status.BadRequest).putHeaders(org.http4s.headers.`Content-Length`.zero) + def server[F[_]]( host: Option[Host], port: Port, @@ -389,6 +392,9 @@ private[server] object ServerHelpers extends ServerHelpersPlatform { requestVault <- if (createRequestVault) mkRequestVault(socket) else Vault.empty.pure[F] resp <- httpApp .run(req.withAttributes(requestVault)) + .recover { case Parser.HeaderP.ParseHeadersError(_) => + badRequest.covary[F] + } .handleErrorWith(errorHandler) .handleError(_ => serverFailure.covary[F]) } yield (req, resp, drain) @@ -534,6 +540,7 @@ private[server] object ServerHelpers extends ServerHelpersPlatform { Applicative[F].pure(None) case err => (err match { + case err: Parser.HeaderP.ParseHeadersError => requestLineParseErrorHandler(err) case err: Parser.Request.ReqPrelude.ParsePreludeError => requestLineParseErrorHandler(err) case err: EmberException.MessageTooLong =>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-wcwh-7gfw-5wrrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59822ghsaADVISORY
- github.com/http4s/http4s/commit/dd518f7c967e5165813b8d4a48a82b8fab852d41ghsax_refsource_MISCWEB
- github.com/http4s/http4s/security/advisories/GHSA-wcwh-7gfw-5wrrghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.