← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2025-59375 · CWE-770 · Disclosed 2025-09-15

libexpat in Expat before 2

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

Like turning a paper straw into a firehose, tiny XML can coerce huge heap growth

CVE-2025-59375 is a resource-exhaustion / denial-of-service bug in libexpat affecting Expat before 2.7.2. A small attacker-supplied XML document can drive disproportionately large dynamic memory allocation inside the parser; upstream documented examples around ~250 KiB input causing ~800 MiB heap use, and the public fuzzing reproducer hit a 2 GiB libFuzzer limit. Upstream also notes that compression wrapped around XML can shrink the attack payload further, which matters for apps that accept compressed uploads, SOAP, SAML, XML APIs, feeds, or file imports.

The vendor/NVD 7.5 HIGH score is technically fair in a vacuum because the bug can be reached over a network with no auth and reliably causes availability loss. In real enterprise patch queues, though, this is not a universal network service flaw; it is a component-level parser DoS that only bites where an exposed application actually feeds attacker-controlled XML into Expat. That reachability friction, plus no KEV listing, very low EPSS, and availability-only impact, pushes this down to MEDIUM for most fleets.

"This is a real XML parser DoS, but it only matters where untrusted XML actually reaches Expat."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find an XML ingestion path

The attacker first needs an application endpoint that accepts untrusted XML and is linked to Expat, such as SOAP handlers, SAML processing, XML file upload/import, RSS/Atom ingest, or device-management APIs. The practical weapon here is usually just a normal client stack like curl, Burp Suite, or a custom HTTP client; the vulnerability is in the parser, not in a separate exploit primitive.
Conditions required:
  • An internet- or tenant-reachable application accepts attacker-controlled XML
  • That application actually uses libexpat for parsing
  • The vulnerable Expat build is present at runtime
Where this breaks in practice:
  • Many hosts with Expat installed never expose XML parsing to untrusted users
  • Modern estates often terminate XML in higher-level frameworks or alternate parsers instead
  • Asset inventories rarely map shared library presence to reachable XML endpoints, which cuts both ways
Detection/coverage: SCA/SBOM tools and distro package scanners will find vulnerable library versions, but most network scanners cannot prove exploitability because libexpat is a backend component, not a bannered service.
STEP 02

Deliver a memory-amplifying XML document

The public OSS-Fuzz/ClusterFuzz issue provides a reproducer and analysis showing that a relatively small XML payload can trigger runaway allocation behavior. Upstream explicitly notes that a compression layer can reduce the minimum payload size further, so a gzip-capable client or standard HTTP upload flow is enough.
Conditions required:
  • The target path passes the supplied XML to Expat largely unfiltered
  • Request/body size limits are permissive enough for the crafted sample
Where this breaks in practice:
  • Reverse proxies, API gateways, and upload limits often reject or truncate oversized requests before the parser sees them
  • Apps that pre-validate XML size/complexity or decompress in a constrained worker reduce reliability
Detection/coverage: WAFs may log anomalous XML uploads, but signature coverage is weak because the behavior is parser amplification rather than a stable exploit string. Fuzzing artifacts are public, which helps red teams and defenders more than commodity scanners.
STEP 03

Force disproportionate heap growth inside Expat

Before 2.7.2, Expat lacks the new allocation tracking and amplification limits added by the fix set. The merged fix train introduced a default activation threshold of 64 MiB and a default maximum amplification factor of 100, rejecting violating documents with an out-of-memory style error instead of letting allocation grow unchecked.
Conditions required:
  • The process is running an affected Expat build before 2.7.2 or an unfixed downstream backport state
  • The service has enough memory headroom to attempt the allocations
Where this breaks in practice:
  • Container memory limits, cgroups, ulimit, or service-level memory governors may kill only the worker/container rather than the host
  • Aggressive watchdogs and autoscaling can mask impact from one-off probes
Detection/coverage: Host telemetry is the best lens here: sharp RSS growth, OOM kills, container restarts, and parser/process crashes. Generic vuln scanning sees presence; runtime monitoring sees exploit attempts.
STEP 04

Turn parser pressure into service disruption

The end state is availability loss, not code execution: a worker crashes, a parser process is OOM-killed, or the service stalls under memory pressure. In most modern deployments the blast radius is the individual service or container handling XML, though sloppy shared-host sizing can still make it a node-level nuisance.
Conditions required:
  • The service is business-relevant and exposed often enough for repeated requests to matter
  • Restart or orchestration behavior does not fully absorb repeated crashes
Where this breaks in practice:
  • Process supervisors, container restarts, and horizontal scaling often convert this from outage to noisy degradation
  • No confidentiality or integrity impact means attackers need repeated pressure for operational effect
Detection/coverage: Look for repeated OOM events, restart loops, upstream 5xx spikes, and XML-heavy request patterns correlated with memory pressure. EDR is not the primary control; app and platform telemetry are.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo public active exploitation evidence found in the reviewed primary sources, and no CISA KEV listing was identified.
Proof-of-concept availabilityPublic reproducer exists via OSS-Fuzz/ClusterFuzz issue #1018; this is enough for reliable DoS testing even without a turnkey exploit kit.
EPSS0.00102 (user-supplied), which is very low and consistent with a niche, availability-only parser bug rather than a high-velocity exploitation candidate.
KEV statusNot listed in the CISA Known Exploited Vulnerabilities Catalog.
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H — unauthenticated network reachability and easy trigger, but availability only.
Affected versionsExpat before 2.7.2 / libexpat before that upstream release.
Fixed versions and backportsUpstream fix is 2.7.2. Distros may backport: Ubuntu lists fixes including 25.10 2.7.1-2ubuntu0.2, 22.04 2.4.7-1ubuntu0.7, and ESM builds for older releases; Debian marks unstable 2.7.2-1 fixed while some stable branches are still marked vulnerable or ignored as a minor issue.
Exposure realityInference: there is no clean internet census for this CVE because Expat is a shared library, not an identifiable network daemon. Exposure depends on whether your internet-facing apps actually hand untrusted XML to Expat.
Disclosure timelineThe public fuzzing issue was opened 2025-08-16; the fix PR merged 2025-09-15; the upstream security release was announced on 2025-09-16. NVD shows the record published on 2025-09-14.
Researcher / reporterDiscovery path points to OSS-Fuzz / ClusterFuzz; upstream security communication and release note came from Sebastian Pipping.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.2/10)

The decisive friction is reachability: this is only exploitable where an attacker can drive untrusted XML into an Expat-backed parser path, which is a much smaller population than 'every host with libexpat installed'. The bug is real and reliable for DoS once reached, but the lack of exploitation evidence and the availability-only, service-scoped blast radius keep it out of the urgent patch lane.

HIGH Technical impact is limited to denial of service / resource exhaustion
MEDIUM Enterprise exposure depends heavily on whether internet-facing apps actually parse attacker-controlled XML with Expat
HIGH Public evidence does not show KEV listing or active in-the-wild exploitation

Why this verdict

  • Downward pressure: requires a real XML parser path — unauthenticated network access only matters if the target application exposes an XML ingest feature that passes attacker content into Expat.
  • Downward pressure: library bug, not service bug — presence of libexpat on disk says little about exploitability; a large fraction of endpoints and servers carrying the package are not directly reachable through untrusted XML.
  • Downward pressure: blast radius is usually local to one service/container — modern orchestration, process supervision, and memory limits often convert this into a restart/noise problem rather than enterprise-wide failure.
  • Upward pressure: public reproducer and easy trigger — the OSS-Fuzz testcase makes reliable validation straightforward, and upstream explicitly warns that compression can reduce the minimum payload size.
  • Upward pressure: broad software embedding — Expat is common in the stack, so when you do have exposed SOAP/SAML/XML import surfaces, this bug can be surprisingly reachable.

Why not higher?

There is no code execution, auth bypass, or data exposure here; the impact is confined to availability. More importantly, the attack chain usually requires a specific exposed XML-handling feature, which implies a much narrower reachable population than the vendor CVSS suggests for a generic network bug.

Why not lower?

Once an attacker finds a reachable Expat-backed XML path, the DoS appears reliable and easy to trigger, and there is a public reproducer. For organizations still running SOAP, SAML, XML uploads, or XML-fed middleware on the edge, this is not theoretical background noise.

05 · Compensating Control

What to do — in priority order.

  1. Cap XML request size and decompression budget — Apply strict body-size, decompressed-size, and parser-complexity limits at the reverse proxy, API gateway, or upload handler. Because this verdict is MEDIUM, there is no mitigation SLA — go straight to the 365-day remediation window; still, do this early on any internet-facing XML endpoint because it reduces both this bug and adjacent parser abuse.
  2. Isolate XML parsing in memory-constrained workers — Run XML-handling services behind cgroup/container memory limits, systemd MemoryMax, or equivalent resource governors so parser abuse kills a worker, not a shared node. For a MEDIUM finding there is no mitigation SLA, but this is the highest-value architectural guardrail if patching must wait.
  3. Reduce exposed XML attack surface — Disable unused SOAP/XML upload/import features, require authentication where possible, and route partner-only XML paths behind VPN or allowlists. This is especially relevant for products that still expose legacy XML workflows externally; again, no mitigation SLA applies for MEDIUM, but exposed XML should not sit wide open.
  4. Prefer vendor backports over naive version greps — Update using your distro/vendor package stream and document approved fixed backports, because some patched builds carry upstream versions lower than 2.7.2. This avoids false positives and keeps verification sane during the 365-day remediation window.
What doesn't work
  • A generic WAF rule is not enough by itself; this bug is about parser-side allocation amplification, not a single stable exploit string.
  • EDR alone will not prevent the issue; at best it will show OOM kills, crashes, or restarts after the fact.
  • A file-size-only control can miss the case entirely because upstream notes that small XML, and especially compressed XML, can still trigger outsized memory use.
06 · Verification

Crowdsourced verification payload.

Run this on the target host or container image that actually ships libexpat. Invoke it as python3 check_expat_cve_2025_59375.py; it needs no admin rights, but it benefits from access to local package-manager metadata. The script checks the runtime Expat version and a few known distro backport package versions, then prints VULNERABLE, PATCHED, or UNKNOWN.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# CVE-2025-59375 verifier for Expat / libexpat
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import ctypes
import ctypes.util
import os
import platform
import re
import shutil
import subprocess
import sys

FIXED_UPSTREAM = (2, 7, 2)
KNOWN_FIXED_PACKAGES = {
    # Ubuntu USN-8022-1 backports
    "2.7.1-2ubuntu0.2",
    "2.4.7-1ubuntu0.7",
    "2.2.9-1ubuntu0.8+esm1",
    "2.2.5-3ubuntu0.9+esm3",
    "2.1.0-7ubuntu0.16.04.5+esm11",
    "2.1.0-4ubuntu1.4+esm11",
}


def run(cmd):
    try:
        p = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        if p.returncode == 0:
            return p.stdout.strip()
    except Exception:
        pass
    return ""


def parse_upstream_version(s):
    if not s:
        return None
    m = re.search(r"(\d+)\.(\d+)\.(\d+)", s)
    if not m:
        return None
    return tuple(int(x) for x in m.groups())


def version_lt(a, b):
    return a < b


def get_runtime_expat_version():
    names = []
    found = ctypes.util.find_library("expat")
    if found:
        names.append(found)
    names.extend([
        "libexpat.so.1",
        "libexpat.so",
        "libexpat.1.dylib",
        "libexpat.dylib",
        "libexpat.dll",
        "expat.dll",
    ])

    tried = []
    for name in names:
        if name in tried:
            continue
        tried.append(name)
        try:
            lib = ctypes.CDLL(name)
            fn = lib.XML_ExpatVersion
            fn.restype = ctypes.c_char_p
            raw = fn()
            if raw:
                text = raw.decode(errors="ignore")
                return text, name
        except Exception:
            continue
    return None, None


def get_package_version():
    # Debian/Ubuntu
    if shutil.which("dpkg-query"):
        for pkg in ["libexpat1", "expat"]:
            out = run(["dpkg-query", "-W", "-f=${Version}", pkg])
            if out:
                return pkg, out

    # RPM-based
    if shutil.which("rpm"):
        for pkg in ["expat", "libexpat"]:
            out = run(["rpm", "-q", "--qf", "%{VERSION}-%{RELEASE}", pkg])
            if out and "is not installed" not in out:
                return pkg, out

    # Alpine
    if shutil.which("apk"):
        out = run(["apk", "info", "-v", "expat"])
        if out:
            line = out.splitlines()[0].strip()
            return "expat", line.replace("expat-", "", 1)

    # Arch
    if shutil.which("pacman"):
        out = run(["pacman", "-Q", "expat"])
        if out:
            parts = out.split()
            if len(parts) >= 2:
                return "expat", parts[1]

    # Homebrew
    if shutil.which("brew"):
        out = run(["brew", "list", "--versions", "expat"])
        if out:
            parts = out.split()
            if len(parts) >= 2:
                return "expat", parts[1]

    return None, None


def main():
    runtime_text, runtime_lib = get_runtime_expat_version()
    runtime_ver = parse_upstream_version(runtime_text or "")
    pkg_name, pkg_ver = get_package_version()

    print(f"platform={platform.platform()}")
    print(f"runtime_lib={runtime_lib or 'not-found'}")
    print(f"runtime_version={runtime_text or 'unknown'}")
    print(f"package={pkg_name or 'unknown'}")
    print(f"package_version={pkg_ver or 'unknown'}")

    # Known distro backports first
    if pkg_ver in KNOWN_FIXED_PACKAGES:
        print("PATCHED")
        sys.exit(0)

    # Upstream runtime version check
    if runtime_ver is not None:
        if version_lt(runtime_ver, FIXED_UPSTREAM):
            print("VULNERABLE")
            sys.exit(1)
        else:
            print("PATCHED")
            sys.exit(0)

    # If only package metadata exists, infer cautiously from upstream-like versions
    pkg_upstream = parse_upstream_version(pkg_ver or "")
    if pkg_upstream is not None:
        if version_lt(pkg_upstream, FIXED_UPSTREAM):
            # Could still be backported on some distros; avoid false confidence
            print("UNKNOWN")
            sys.exit(2)
        else:
            print("PATCHED")
            sys.exit(0)

    print("UNKNOWN")
    sys.exit(2)


if __name__ == "__main__":
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not treat this like an emergency fleet-wide fire drill. Use application inventory and SBOM data to find internet-facing XML parsers that actually use Expat, patch those first in the next normal cycle, and move the rest through standard library hygiene. For a MEDIUM verdict there is noisgate mitigation SLA: no mitigation SLA — go straight to the 365-day remediation window; the noisgate remediation SLA is ≤ 365 days. If you do run exposed SOAP/SAML/XML upload paths, add size/decompression and memory caps now even if you wait for the package update.

Sources

  1. NVD entry for CVE-2025-59375
  2. oss-security release announcement
  3. Expat 2.7.2 changelog
  4. OSS-Fuzz / ClusterFuzz issue #1018
  5. Fix PR #1034
  6. Debian security tracker
  7. Ubuntu security notice USN-8022-1
  8. CISA Known Exploited Vulnerabilities Catalog
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.