High priorityDisclosed

Advisory · CVE-2026-93990

Expat accepts malformed UTF-16, enabling XML injection

Expat through 2.8.4 fails to validate low surrogates after high surrogates in UTF-16 input, letting lone high surrogates swallow following code units and hide markup from the parser. Fixed upstream via PR #1282.

Vendor
libexpat
Product
Expat
Identifier / CWE
CVE-2026-93990
CWE-176
Action timing
Immediate
ELI5

Explain it like I’m five

Expat reads XML like a careful reader reading aloud, but it was letting fake words slip past the front door. An attacker can craft text that tricks the reader into skipping real words, so forbidden markup gets smuggled inside.

SIMPLIFIED_ATTACK_PATH04 STEPS
  1. 01Crafted UTF-16

    The attacker sends XML encoded as UTF-16 containing a lone high surrogate not followed by a valid low surrogate.

  2. 02Validation skipped

    Expat through 2.8.4 accepts the malformed sequence instead of rejecting it.

  3. 03Code units swallowed

    The lone high surrogate consumes the following code units, shifting how the remaining bytes are interpreted.

  4. 04Markup hidden

    Characters that form real XML markup are misread, so the parser's view of the document no longer matches the attacker's, enabling XML injection.

What happened

Expat, the XML parser embedded in an enormous share of software (including Python’s bundled parser), mishandles UTF-16 input. According to the disclosure, Expat through version 2.8.4 fails to validate the low surrogate that must follow a high surrogate in a UTF-16 surrogate pair. A lone high surrogate consumes the following code units, which lets an attacker hide markup characters from the parser and smuggle injected XML past validation (CWE-176).

The project has fixed the issue upstream in PR #1282 (commit ff6e1d7e). The advisory covers Expat releases through 2.8.4; consult the project’s releases for a version that includes the fix.

What to do

  1. Inventory where Expat is in use: system packages, language runtimes (Python ships its own copy), browsers, and any software that bundles the library.
  2. Update to an Expat release that includes the upstream fix (PR #1282) per the project release guidance.
  3. Where bundled copies cannot be updated immediately, check with the upstream vendor of that product for a fixed release.
  4. If you parse untrusted XML over UTF-16, consider forcing UTF-8 input handling as a stopgap where the application supports it.
  5. Review recent XML-processing logs for anomalous or malformed UTF-16 input if you operate services that accept XML from the internet.

Management note

Expat is the kind of library nobody inventories until it breaks: it ships inside runtimes, operating systems, and appliances. The blast radius of this bug is proportional to how many XML pipelines your stack accepts untrusted input through. Patching the library is only half the job; finding every bundled copy is the other half.