Neutron, our AI engine, scored 97.1% on UC Berkeley's CyberGym benchmark. Learn more

Deep Dive

Deep Dive

libexpat Billion Laughs Bypass, Fixed in 2.10.0 (CVE-2026-115506)

libexpat through 2.9.0 detects some billion-laughs attacks too late. Learn the affected parser paths, four flaws, and how upgrading to 2.10.0 helps.

TL;DR

  • libexpat 2.10.0, released November 2, 2026, fixes four security flaws. The biggest is CVE-2026-115506, a billion-laughs variant the existing protection detects too late. The other three are a signed integer overflow in storeAtts (CVE-2026-118370), a denial of service through XML_ParserReset (CVE-2026-113351) and a NULL dereference on empty subparser input (CVE-2026-117610).
  • Every release through 2.9.0, the previous release, is affected. The CVE records score CVE-2026-115506 8.7 High, CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N, and the other three 8.2 High.
  • No exploitation is known, and none of the four CVEs is in CISA's Known Exploited Vulnerabilities catalog as of November 2, 2026. Hanno Böck's public payload fuzzer generated the billion-laughs variants that found the detection gap.
  • Upgrade to 2.10.0, or wait for your distribution's backport of the four fixes. Anyone bundling a copy of Expat should update the vendored tree, as the maintainer asks.

Am I affected?

All of these must hold:

  • You run libexpat 2.9.0 or older, or software that bundles it. Python's xml.parsers.expat module links Expat, and the CPython repository itself vendors a copy at Modules/expat.
  • Your software parses attacker-controlled XML documents, or uses a library that does.
  • The specific flaw sets the bar differently: CVE-2026-115506 needs only a moderately sized document, while CVE-2026-118370 needs a multi-gigabyte document whose start tag carries more than 2^30 attributes.

The maintainer states Expat is "packaged everywhere", and lists it as one of the two most widely used libre XML parsers in C. On the systems themselves, distro packages carry it as a runtime library, and the Python, Perl and Apache stacks sit on top of it. Debian's security tracker already lists the expat package as vulnerable in every supported suite.

Timeline of the libexpat 2.10.0 security release: the first CVE published October 20, fixes merged October 23 to 29, and the release shipped November 2, 2026.
Figure 1: Thirteen days passed from the first published CVE to the release that fixes all four. Source: CVE records, libexpat pull requests and the 2.10.0 release, checked November 2, 2026.

Branch Affected versions Fixed version
libexpat stable 2.7.2 through 2.9.0 (CVE-2026-113351); 1.95.2 through 2.9.0 (the other three) 2.10.0 (2026-11-02)

Check the version you link. Debian and Ubuntu ship it as libexpat1; the Python interpreter reports the version of the copy it links:

dpkg -l libexpat1
python3 -c "import xml.parsers.expat as e; print(e.EXPAT_VERSION)"

A print like expat_2.9.0 means the affected release. Debian's security tracker marks the expat package vulnerable and unfixed in bookworm, trixie, forky and sid as of November 2, 2026.

How do I fix it?

Upgrade to libexpat 2.10.0.

  1. If you install from your distribution, wait for its security update and apply it when it lands; the maintainer's advisory notes the release makes all four fixes available for backporting.
  2. If you build from source or vendor a copy, update the vendored tree to the R_2_10_0 tag and rebuild every consumer. The maintainer's advisory asks package maintainers and bundlers to update to 2.10.0.
  3. If you maintain a backport, pull request #1439 carries the follow-up the maintainer flags as essential when backporting the billion-laughs fix.

Upgrade remains the primary fix. Expat supports disabling general entities at build time with XML_GE, available since 2.6.0. At runtime, XML_SetDefaultHandler suppresses expansion of internally defined general entities in text, but its behavior and availability depend on the embedding application or binding. These are not a universal runtime switch for deployed applications. Applications that do not need DTDs can reject DTD-bearing documents before parsing, at the cost of rejecting legitimate DTD documents. See the Expat API documentation.

What is the root cause?

The four flaws sit in expat/lib/xmlparse.c, the parser core. This section quotes the vulnerable code at the 2.9.0 tag.

CVE-2026-115506: the amplification check runs after the expansion work. Expat counts direct bytes and indirect bytes per token as it parses, and rejects when the ratio exceeds a threshold. The check function is:

static XML_Bool
accountingDiffTolerated(XML_Parser originParser, int tok, const char *before,
                        const char *after, int source_line,
                        enum XML_Account account) {
  ...
  *additionTarget += bytesMore;
  const XmlBigCount countBytesOutput
      = rootParser->m_accounting.countBytesDirect
        + rootParser->m_accounting.countBytesIndirect;
  const float amplificationFactor
      = accountingGetCurrentAmplification(rootParser);
  const XML_Bool tolerated
      = (countBytesOutput < rootParser->m_accounting.activationThresholdBytes)
        || (amplificationFactor
            <= rootParser->m_accounting.maximumAmplificationFactor);
  ...
  return tolerated;
}

The function is called from the token-processing paths, which means Expat has already expanded the entity content for a token by the time the counters are updated. Hanno Böck's fuzzer lololol found parameter combinations where the parser performs tens of seconds of expansion before the cumulative ratio crosses the threshold. The fix, pull request #1433, adds an expandedSize field to each entity and pre-computes how far an entity will expand before expansion starts. The rejection then happens up front, and pull request #1439 extends the scan to CDATA sections, comments and processing instructions inside entity values.

CVE-2026-118370: two signed counters in storeAtts overflow. At 2.9.0 the function declares:

static enum XML_Error
storeAtts(XML_Parser parser, const ENCODING *enc, const char *attStr,
          TAG_NAME *tagNamePtr, BINDING **bindingsPtr,
          enum XML_Account account) {
  DTD *const dtd = parser->m_dtd; /* save one level of indirection */
  int attIndex = 0;
  XML_Char *uri;
  int nPrefixes = 0;

Both int counters wrap at 2^31 attributes. A crafted start tag with more than 2^30 attributes makes the arithmetic drive writes out of bounds, corrupting the heap. The fix, pull request #1450, changes both to size_t and adds explicit INT_MAX bounds checks where the values are stored into int-typed parser fields.

CVE-2026-113351: XML_ParserReset never offsets the allocation tracker. Expat 2.7.2 added a heap tracker that tolerates allocations in proportion to document size. Resetting a parser for reuse keeps pooled memory, but the tracker's bytesAllocated total keeps the old document's allocations. A subsequent ordinary document on the same parser can then be rejected with XML_ERROR_NO_MEMORY. The fix, pull request #1431, records bytesAllocatedWhenLastReset and subtracts it, so each document starts from zero.

CVE-2026-117610: a missing switch case for empty input. externalEntityInitProcessor2 at 2.9.0 handles XML_TOK_BOM, XML_TOK_PARTIAL and others, but has no XML_TOK_NONE case. An empty, non-final first parse on an external entity subparser falls through to externalEntityInitProcessor3, and a later parse crashes. Pull request #1444 adds the case and returns XML_ERROR_NONE cleanly.

How is it exploited?

No full exploit is published for any of the four. The attack inputs come from the CVE records and the maintainer's advisory.

  1. The attacker sends a moderately sized XML document with a large DTD and nested entity expansions. The public fuzzer sizes such documents to stay under the detection threshold as long as possible, and the parse consumes tens of seconds of CPU before accountingDiffTolerated rejects it (CVE-2026-115506).
  2. The attacker submits a multi-gigabyte document whose single start tag carries more than 2^30 attributes. The int counters in storeAtts wrap, and the attribute copy loop writes past its buffers (CVE-2026-118370).
  3. The attacker parses a large document on a parser the application reuses through XML_ParserReset, then a second ordinary document fails with XML_ERROR_NO_MEMORY until the parser is recreated (CVE-2026-113351).
  4. The attacker controls the chunking of an external entity's input stream and starts it with an empty, non-final chunk, crashing the process on the next parse (CVE-2026-117610).

The fuzzer that generated the attack documents for step 1 is public: github.com/hannob/lololol.

How does the attack flow?

One sentence: a crafted document with a deep entity tree expands faster than the accounting check notices, so the parser burns CPU before rejecting, or the attribute counters wrap into an out-of-bounds write.

flowchart LR A["Attacker-controlled
XML document"] --> B["Entity expansion
adds indirect bytes"] B --> C["Amplification check runs
after each token only"] C --> D["Expansion work happens
before rejection"] D --> E["Tens of seconds of CPU
per parse"]

What can an attacker do?

The documented impacts are denial of service on the parsing process and, for CVE-2026-118370, out-of-bounds heap writes:

  • The attacker needs no privileges or user interaction beyond delivering a document to a system that parses it. All four CVSS v4.0 vectors record a network attack vector.
  • CVE-2026-115506 and CVE-2026-113351 are denial-of-service flaws. The CVE records describe them as such, and the maintainer's advisory rates no code-execution impact.
  • CVE-2026-118370 is memory corruption with a demanding precondition: a multi-gigabyte document with more than 2^30 attributes in one start tag. The CVE record stops at "crashing applications or corrupting heap memory", and no source demonstrates code execution.
  • CVE-2026-117610 crashes the process when the attacker controls stream chunking, which the CVE record notes as a precondition.

Why does the fix work?

The four pull requests close the four mechanisms.

For CVE-2026-115506, pull request #1433 pre-computes each entity's expandedSize before expansion starts, and the accounting check consults the projected size instead of waiting for the byte counters:

  /* Indirect accounting bytes charged when this entity is expanded: 1 per text
     character, plus each reference's "&name;"/"%name;" token length and its own
     expansion. ... */
  XmlBigCount expandedSize;

For CVE-2026-118370, pull request #1450 changes the two counters and bounds the stores:

-  int attIndex = 0;
+  size_t attIndex = 0;
   XML_Char *uri;
-  int nPrefixes = 0;
+  size_t nPrefixes = 0;

For CVE-2026-113351, pull request #1431 adds the reset watermark:

+  size_t bytesAllocatedWhenLastReset;

and XML_ParserReset now records the allocation total at reset time, so the next document's tolerance check subtracts what the previous document left behind.

For CVE-2026-117610, pull request #1444 adds the missing case:

+  case XML_TOK_NONE:
+    if (! parser->m_parsingStatus.finalBuffer) {
+      *endPtr = start;
+    }
+    return XML_ERROR_NONE;

The maintainer notes the release closes 12 security issues in total against 20 new findings this period, with 13 known security issues still unfixed at release time. The four fixes are complete for the mechanisms they address; whether further billion-laughs variants remain is exactly what the fuzzer continues to probe.

How do I check if I were compromised?

None of the four flaws leaks data by design, so the checks focus on availability anomalies.

  1. Review logs for XML parsers handling untrusted input: XML_ERROR_NO_MEMORY may warrant investigation for CVE-2026-113351; prolonged CPU consumption during parsing and XML_ERROR_AMPLIFICATION_LIMIT_BREACH errors may warrant investigation for CVE-2026-115506. These observations alone do not prove exploitation.
  2. Look for requests delivering unusually large XML payloads to services that parse them, including any multi-gigabyte submission (CVE-2026-118370).
  3. For CVE-2026-117610, check crash dumps in external-entity subparser paths.

An attempted billion-laughs document that Expat rejected leaves no trace beyond the rejection error, and a rejected document does not mean the service was degraded.

Timeline

  • 2026-10-20: CVE-2026-113351 is published for the XML_ParserReset denial of service, reported by Pu Cheng.
  • 2026-10-22: CVE-2026-115506 is published for the billion-laughs detection gap, after Hanno Böck's fuzzer found the variants.
  • 2026-10-23: The XML_ParserReset fix, pull request #1431, merges.
  • 2026-10-26: The billion-laughs follow-up, pull request #1439, merges.
  • 2026-10-28: CVE-2026-117610 and CVE-2026-118370 are published.
  • 2026-10-29: The remaining fixes, pull requests #1444 and #1450, merge.
  • 2026-11-02: libexpat 2.10.0 ships with all four fixes.

FAQ

Which of the four should I prioritize?

CVE-2026-115506. Its input is a moderately sized document any XML endpoint accepts, and its cost is tens of seconds of CPU per parse. CVE-2026-118370 is memory corruption, but its multi-gigabyte precondition makes it far harder to deliver.

Does this affect Python applications?

It affects any process that links an affected Expat, including Python via xml.parsers.expat. The interpreter reports the linked version with EXPAT_VERSION. On system-package Python builds, upgrading the system libexpat1 updates the interpreter's parser.

Was any of this exploited in the wild?

No exploitation is reported for any of the four, and none is in CISA's KEV catalog as of November 2, 2026. The public fuzzer demonstrates the attack, not real use.

Why does the CVE list a 2.7.2 floor for one flaw and no floor for the others?

The allocation tracker that CVE-2026-113351 breaks was added in 2.7.2, so that flaw cannot exist earlier. The other three predate their protections or live in code paths present since before 2.0, so the records say "through 2.9.0".

Can I just disable entity expansion?

Build-time and runtime controls differ: XML_GE controls general-entity support at build time, while XML_SetDefaultHandler changes internal general-entity expansion in text. Check what your application or binding exposes and test compatibility; upgrade to 2.10.0 remains the primary fix.

Who found and fixed these?

Hanno Böck found the billion-laughs variants with his fuzzer, and Pu Cheng reported the reset flaw. Shani Yosef brought the key idea for the pre-expansion fix, as the release advisory and the CVE credits record.

Find where Expat runs in your estate with Ostorlab

Ostorlab's dependency scans build the inventory of the packages your services and container images install, so you can find every deployment carrying an affected libexpat. Run a scan with Ostorlab to confirm the 2.10.0 or backported update everywhere it matters.

References

  • Expat maintainer's release advisory, "Expat 2.10.0 released, fixes four vulnerabilities", published November 2, 2026, with the attack explanation, credits and the backporting note for pull request #1439: https://blog.hartwork.org/posts/expat-2-10-0-released/ (checked November 2, 2026)
  • oss-security release announcement, "libexpat 2.10.0 fixes two vulnerabilities", published November 2, 2026, listing the four CVEs, the change log entries and the fixing pull requests: https://www.openwall.com/lists/oss-security/2026/11/02/6 (checked November 2, 2026)
  • Expat 2.10.0 release page on GitHub, published November 2, 2026: https://github.com/libexpat/libexpat/releases/tag/R_2_10_0 (checked November 2, 2026)
  • CVE-2026-115506 record, published October 22, 2026, CVSS v4.0 8.7 High and CVSS v3.1 7.5 High: https://www.cve.org/CVERecord?id=CVE-2026-115506 (checked November 2, 2026)
  • CVE-2026-113351 record, published October 20, 2026, CVSS v4.0 8.2 High, CVSS v3.1 5.9 Medium: https://www.cve.org/CVERecord?id=CVE-2026-113351 (checked November 2, 2026)
  • CVE-2026-117610 record, published October 28, 2026, CVSS v4.0 8.2 High, CVSS v3.1 5.9 Medium: https://www.cve.org/CVERecord?id=CVE-2026-117610 (checked November 2, 2026)
  • CVE-2026-118370 record, published October 28, 2026, CVSS v4.0 8.2 High, CVSS v3.1 5.9 Medium: https://www.cve.org/CVERecord?id=CVE-2026-118370 (checked November 2, 2026)
  • Fix pull request #1431 for CVE-2026-113351, merged October 23, 2026: https://github.com/libexpat/libexpat/pull/1431 (checked November 2, 2026)
  • Fix pull request #1433 for CVE-2026-115506, the pre-computed expansion size, and its follow-up #1439 for CDATA, comments and processing instructions: https://github.com/libexpat/libexpat/pull/1433 (checked November 2, 2026)
  • Fix pull request #1444 for CVE-2026-117610, merged October 29, 2026: https://github.com/libexpat/libexpat/pull/1444 (checked November 2, 2026)
  • Fix pull request #1450 for CVE-2026-118370, merged October 29, 2026: https://github.com/libexpat/libexpat/pull/1450 (checked November 2, 2026)
  • Hanno Böck's payload fuzzer for billion-laughs variants, linked from the Expat change log: https://github.com/hannob/lololol (checked November 2, 2026)
  • Expat project homepage, describing Expat as packaged everywhere and listing users, bindings and wrappers: https://libexpat.github.io/ (checked November 2, 2026)
  • Vulnerable source of expat/lib/xmlparse.c at the R_2_9_0 tag, for the quoted code: https://github.com/libexpat/libexpat/blob/R_2_9_0/expat/lib/xmlparse.c (checked November 2, 2026)
  • Debian security tracker for CVE-2026-115506, marking expat vulnerable and unfixed in bookworm, trixie, forky and sid as of November 2, 2026: https://security-tracker.debian.org/tracker/CVE-2026-115506 (checked November 2, 2026)
  • Ubuntu security page for CVE-2026-115506, listing the vulnerable expat source package and the packages that use the system expat: https://ubuntu.com/security/CVE-2026-115506 (checked November 2, 2026)
  • CISA Known Exploited Vulnerabilities catalog JSON feed, searched for all four CVEs with no match as of November 2, 2026: https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (checked November 2, 2026)