Skip to main content
Vulnerability Database/CVE-2026-93990

CVE-2026-93990: Expat XML Parser UTF-16 XXE Vulnerability

CVE-2026-93990 is an XML External Entity flaw in Expat parser that fails to validate UTF-16 surrogates, enabling attackers to inject malicious XML. This post explains its impact, affected versions, and mitigation steps.

Published:

CVE-2026-93990 Overview

CVE-2026-93990 is an input validation flaw in the Expat XML parser through version 2.8.4. The library fails to validate low surrogates that follow high surrogates in UTF-16 encoded input. Attackers can craft malformed UTF-16 XML documents containing lone high surrogates that consume the following code unit. This behavior hides markup characters from the parser and enables XML injection against applications that rely on Expat for parsing. The weakness maps to CWE-176: Improper Handling of Unicode Encoding.

Critical Impact

Malformed UTF-16 sequences can smuggle XML markup past Expat, allowing injection of attacker-controlled elements or attributes into parsed documents.

Affected Products

  • Expat (libexpat) versions up to and including 2.8.4
  • Applications and language bindings that embed Expat for UTF-16 XML parsing
  • Downstream distributions that ship the vulnerable Expat library

Discovery Timeline

  • 2026-09-19 - CVE-2026-93990 published to the National Vulnerability Database
  • 2026-09-22 - Last updated in NVD database

Technical Details for CVE-2026-93990

Vulnerability Analysis

Expat is a widely used stream-oriented XML parser written in C. When processing UTF-16 encoded XML, the parser must correctly decode surrogate pairs in the range U+D800 to U+DFFF. A valid surrogate pair consists of a high surrogate followed by a low surrogate. Expat through 2.8.4 decodes the high surrogate and advances past the following code unit without confirming it is a valid low surrogate. An attacker can insert a lone high surrogate before markup bytes such as <, >, or quote characters. The parser then treats those markup bytes as continuation of the surrogate pair and skips them. Downstream consumers observing the reserialized or interpreted document see a different structure than what the parser processed, breaking the trust boundary between parsing and application logic.

Root Cause

The root cause is missing validation of the second UTF-16 code unit during surrogate decoding in expat/lib/xmltok_impl.c. The IS_INVALID_CHAR check for UTF-16 leads was defined as a no-op, so malformed sequences passed through without rejection. Pull request #1282 adds an explicit high-surrogate check to reject unpaired surrogates.

Attack Vector

Exploitation requires the target application to parse attacker-controlled UTF-16 XML with Expat. The attack is network-reachable, requires no privileges, and needs no user interaction. Injected markup can alter document semantics used for authentication decisions, configuration, or serialized data exchange.

c
// Patch excerpt from expat/lib/xmltok_impl.c
// Merge pull request #1282: utf16-high-surrogate-check

#ifdef XML_TOK_IMPL_C

-#  ifndef IS_INVALID_CHAR // i.e. for UTF-16 and XML_MIN_SIZE not defined
-#    define IS_INVALID_CHAR(enc, ptr, n) (0)
-#  endif
-
#  define INVALID_LEAD_CASE(n, ptr, nextTokPtr)                                \
  case BT_LEAD##n:                                                             \
    if (end - ptr < n)                                                         \

Source: GitHub commit ff6e1d7. The removed no-op macro previously allowed malformed UTF-16 leads through unchecked; the patched code enforces surrogate validation.

Detection Methods for CVE-2026-93990

Indicators of Compromise

  • UTF-16 encoded XML payloads containing high surrogate code units (0xD800-0xDBFF) that are not immediately followed by a valid low surrogate (0xDC00-0xDFFF).
  • XML documents where reserialized output differs structurally from the raw input, indicating parser-level markup hiding.
  • Application logs showing Expat parse results that conflict with downstream validators or canonicalizers.

Detection Strategies

  • Inventory processes and containers using libexpat at version 2.8.4 or earlier through software composition analysis.
  • Add pre-parse validation that rejects UTF-16 inputs containing unpaired surrogates before handing data to Expat.
  • Compare parsed element and attribute counts against a strict schema to surface injected markup.

Monitoring Recommendations

  • Log XML parser errors and warnings from services accepting UTF-16 input, and alert on sudden increases.
  • Capture and retain sample payloads at ingress points that declare encoding="UTF-16" for offline inspection.
  • Track outbound behavior of services that consume parsed XML for anomalies in downstream authorization or data flow.

How to Mitigate CVE-2026-93990

Immediate Actions Required

  • Upgrade Expat to a release that includes the fix from pull request #1282 once published by the maintainers.
  • Rebuild and redistribute any statically linked binaries, language bindings, or container images that bundle Expat.
  • Restrict or reject UTF-16 encoded XML at application ingress where UTF-8 is acceptable.

Patch Information

The fix is committed as ff6e1d7e750bbe245178f51a47a965dc8342861a in the libexpat repository. The patch removes the no-op IS_INVALID_CHAR macro path for UTF-16 and enforces validation of the low surrogate that must follow a high surrogate. Additional details are available in the VulnCheck advisory.

Workarounds

  • Transcode incoming XML to UTF-8 before parsing, rejecting any input that contains unpaired surrogates during conversion.
  • Enforce a strict allowlist of accepted XML encodings at the application boundary.
  • Apply schema validation with canonicalization after parsing to detect structural drift caused by hidden markup.
bash
# Example: verify installed Expat version and audit linked binaries
dpkg -l | grep -i expat
rpm -qa | grep -i expat

# Identify processes that have libexpat mapped in memory
for pid in $(pgrep -a . | awk '{print $1}'); do
  grep -l 'libexpat' /proc/$pid/maps 2>/dev/null && echo "pid=$pid"
done

# Pre-parse guard: reject UTF-16 XML containing unpaired high surrogates
python3 -c '
import sys
data = sys.stdin.buffer.read().decode("utf-16", errors="strict")
print("ok", len(data))
'

Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.