A booby trap that only fires when the building is already on fire
CVE-2022-43680 is a use-after-free (CWE-416) in libexpat ≤ 2.4.9, the ubiquitous C XML parsing library embedded in Python, Apache httpd, PHP, Git, and hundreds of other packages. When XML_ExternalEntityParserCreate calls parserCreate and that inner function hits an allocation failure (out-of-memory), cleanup code incorrectly destroys a DTD structure shared with the root parser. Subsequent access to the freed DTD crashes the process. The fix — shipping in libexpat 2.5.0 (October 25, 2022) — simply moves the m_isParamEntity flag into parserCreate so cleanup knows not to free the shared DTD. Affected: all libexpat versions through 2.4.9.
The vendor CVSS of 7.5 HIGH is technically defensible on paper — the vector shows AV:N/AC:L/PR:N/UI:N — but it overstates practical risk. The entire chain depends on the target process being in an out-of-memory state at the exact moment it parses a crafted external entity. That is an attacker-unfriendly precondition: you either need a separate memory-exhaustion primitive or dumb luck. Impact is limited to availability only (C:N/I:N/A:H) — no code execution, no data leak. Red Hat independently rated this Moderate, and nearly four years later there is still no public PoC, no weaponized exploit, no in-the-wild activity, and no KEV listing. The 7.5 label is scanner noise; real-world risk is medium at best.
4 steps from start to impact.
Identify a libexpat-consuming service
xml.parsers.expat, or any SOAP/REST endpoint accepting XML input. The service must process external entities (DTDs) for the vulnerable code path to be reached.- Network-reachable XML parsing endpoint
- libexpat ≤ 2.4.9 linked into the service
- External entity processing enabled (not disabled via
XML_SetEntityDeclHandleror similar)
- Many modern frameworks disable external entity processing by default (XXE hardening)
- Python's
defusedxmland similar wrappers block external entities entirely
Induce out-of-memory on target process
parserCreate fails a memory allocation. The attacker must push the target process into an OOM state — e.g., by flooding it with large payloads, exploiting a separate memory leak, or targeting a container with tight memory limits (cgroups). This is a separate, prerequisite exploit that the CVSS vector does not capture.- Ability to send enough data or requests to exhaust process memory
- No upstream rate-limiting, WAF, or request-size caps blocking the flood
- Most production deployments sit behind load balancers, WAFs, and reverse proxies with request-size limits
- Container orchestrators (Kubernetes) will OOM-kill and restart the pod before the attacker can chain the next step
- OS-level memory overcommit means the OOM killer may terminate a different process entirely
dmesg/journald, container restart metrics in Prometheus/Datadog.Send crafted XML with external entity during OOM window
XML_ExternalEntityParserCreate calls parserCreate, which fails the m_protocolEncodingName allocation. The error-handling path calls XML_ParserFree on a parser whose m_isParamEntity flag hasn't been set yet, so it destroys the shared DTD.- OOM condition active at the exact moment the XML is parsed
- External entity reference in the payload reaches the vulnerable code path
- Timing window is extremely narrow — the OOM must persist through the allocation attempt
- If OOM triggers before reaching the parser, the service returns a 503 or connection reset, not the vulnerable path
Process crashes (DoS achieved)
- Steps 1-3 succeeded in sequence
- Process restarts automatically in most production environments (systemd, supervisord, Kubernetes)
- No confidentiality or integrity impact — attacker gains nothing beyond a crash
The supporting signals.
| In-the-Wild Exploitation | None observed. No CISA KEV listing, no reports from Mandiant, CrowdStrike, or other threat-intel vendors. Nearly 4 years post-disclosure with zero documented exploitation. |
|---|---|
| Proof-of-Concept | No public PoC. The upstream GitHub issue (#649) describes the bug mechanically but no standalone exploit code has been published. The OOM precondition makes PoC construction non-trivial. |
| EPSS Score | 0.02445 (2.4%) — 83rd percentile. Elevated relative to the average CVE but still well below the ~6%+ threshold where exploitation becomes common. |
| KEV Status | Not listed in CISA KEV as of 2026-09-14. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H — 7.5 HIGH. Note: the vector does not model the OOM precondition, which effectively raises Attack Complexity beyond Low in practice. |
| Affected Versions | libexpat ≤ 2.4.9 (all prior versions). Embedded copies in Python (CPython), Apache httpd, PHP, Git, and many other packages are transitively affected. |
| Fixed Version | libexpat 2.5.0 (released 2022-10-25). Distro backports: RHEL 8/9 via expat-2.2.5-11.el8 / expat-2.5.0-1.el9, Debian Bullseye via 2.2.10-2+deb11u5, Ubuntu 22.04 via 2.4.7-1ubuntu0.1. |
| Scanning / Exposure | libexpat is installed on virtually every Linux host, but direct network exposure depends on whether an XML-parsing service (httpd, Python app, etc.) is reachable. No Shodan/GreyNoise signatures specific to this CVE exist because exploitation is not payload-detectable. |
| Disclosure Date | 2022-10-24 (NVD published). Fix released next day (2022-10-25). |
| Researcher / Credit | Reported via the libexpat project. Upstream fix by Sebastian Pipping in PRs #616 and #650. |
noisgate verdict.
The single most decisive factor is the out-of-memory precondition: the entire chain requires the target process to already be memory-exhausted, a condition the attacker must induce via a separate, unrelated attack primitive — effectively doubling the exploit chain length and narrowing the reachable population to near-zero in production environments with standard resource controls. Impact is capped at denial-of-service with no path to code execution, confidentiality breach, or integrity violation.
Why this verdict
- OOM precondition is a hidden Attack Complexity multiplier. The CVSS vector says AC:L, but in practice the attacker needs the process to fail a malloc at a specific point in the parser lifecycle. This is closer to AC:H, which alone would drop the base score from 7.5 to 5.9.
- Impact ceiling is Availability-only. C:N/I:N/A:H means even a successful exploit yields a process crash — no data exfiltration, no lateral movement, no persistence. In containerized or systemd-managed environments the service auto-restarts in seconds.
- No weaponization in ~4 years. Zero public PoCs, zero KEV entries, zero threat-intel reports. The EPSS of 2.4% is modest and consistent with a bug that researchers understand but attackers ignore.
- Role multiplier: libexpat is embedded everywhere — Python runtimes, Apache httpd, Git — so it touches high-value roles (CI/CD runners, web frontends, identity-adjacent services). However, the impact in ALL roles is capped at a transient crash. A CI runner crash delays a build; it does not leak secrets or pivot supply chains. A web server crash behind a load balancer causes a blip, not an outage. Because the blast radius is host-local, transient, and availability-only even in high-value roles, the floor does not elevate above MEDIUM.
- Red Hat independent assessment agrees. Red Hat rated this Moderate despite the 7.5 CVSS, explicitly citing the OOM precondition as the reason.
Why not higher?
Promoting to HIGH would require either a credible code-execution path or evidence of real-world exploitation — neither exists. The use-after-free targets a DTD structure, not a vtable or function pointer, and no researcher has demonstrated heap grooming to achieve control. Four years of silence from attackers confirms the theoretical risk has not translated to practical threat.
Why not lower?
Dropping to LOW or IGNORE would undercount the sheer ubiquity of libexpat across the Linux ecosystem. While the OOM gate makes remote exploitation difficult, it is not impossible — especially in memory-constrained containers with aggressive cgroup limits. The bug is real, the fix is trivial, and leaving unpatched libexpat on thousands of hosts is unnecessary hygiene debt.
What to do — in priority order.
- Disable external entity processing in XML parsers — Set
XML_SetExternalEntityRefHandlerto a no-op or use wrapper libraries like Python'sdefusedxmlthat block external entities by default. This eliminates the vulnerable code path entirely. Deploy across your fleet as part of normal code-hardening — no emergency mitigation SLA applies for a MEDIUM verdict. - Enforce container memory limits with restart policies — Ensure all containerized workloads have explicit memory limits (
resources.limits.memoryin Kubernetes) and automatic restart-on-OOM. This makes the OOM precondition self-correcting — the container dies and restarts before the attacker can chain the UAF. - Set request-size limits on XML-accepting endpoints — Configure reverse proxies (nginx
client_max_body_size, ApacheLimitRequestBody) to cap XML payloads at a reasonable size (e.g., 10 MB). This makes it harder for an attacker to induce OOM via payload flooding. - Update libexpat to ≥ 2.5.0 — The definitive fix. On RHEL/CentOS:
yum update expat. On Debian/Ubuntu:apt update && apt upgrade libexpat1. On Python-centric stacks, ensure CPython ≥ 3.11.1 or ≥ 3.10.9 which bundle expat 2.5.0. No mitigation SLA — go straight to the 365-day remediation window per noisgate MEDIUM policy.
- WAF XML-injection rules — the trigger is not a malicious payload pattern; it is an environmental condition (OOM) combined with a normal external-entity reference. No WAF signature can detect or block it.
- Network segmentation — libexpat runs inside the application process, not as a separate service. Segmenting the network does not change whether the library is vulnerable.
Crowdsourced verification payload.
Run on each target Linux host as any user with read access to the package database. Example: bash check_cve_2022_43680.sh. No root required. Works on RHEL/CentOS, Debian/Ubuntu, and Alpine.
#!/usr/bin/env bash
# check_cve_2022_43680.sh — Detect CVE-2022-43680 (libexpat UAF)
# Run on target Linux host. No root needed.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
VULN_THRESHOLD="2.5.0"
version_gte() {
# Returns 0 if $1 >= $2 using sort -V
printf '%s\n%s' "$2" "$1" | sort -V -C
}
get_expat_version() {
# Try package managers first
if command -v rpm &>/dev/null; then
ver=$(rpm -q --qf '%{VERSION}' expat 2>/dev/null || true)
if [[ -n "$ver" && "$ver" != *"not installed"* ]]; then
echo "$ver"; return 0
fi
fi
if command -v dpkg-query &>/dev/null; then
ver=$(dpkg-query -W -f='${Version}' libexpat1 2>/dev/null || true)
if [[ -n "$ver" ]]; then
# Strip epoch and Debian revision for comparison
ver=$(echo "$ver" | sed 's/^[0-9]*://; s/-[^-]*$//')
echo "$ver"; return 0
fi
fi
if command -v apk &>/dev/null; then
ver=$(apk info -e expat 2>/dev/null | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
if [[ -n "$ver" ]]; then
echo "$ver"; return 0
fi
fi
# Fallback: check libexpat.so
if ldconfig -p 2>/dev/null | grep -q libexpat; then
ver=$(strings $(ldconfig -p | grep libexpat | awk '{print $NF}' | head -1) 2>/dev/null | grep -oP 'expat_[0-9]+\.[0-9]+\.[0-9]+' | head -1 | sed 's/expat_//' || true)
if [[ -n "$ver" ]]; then
echo "$ver"; return 0
fi
fi
return 1
}
VER=$(get_expat_version) || { echo "UNKNOWN — could not determine libexpat version"; exit 2; }
echo "Detected libexpat version: $VER"
echo "Fixed in: >= $VULN_THRESHOLD"
if version_gte "$VER" "$VULN_THRESHOLD"; then
echo "PATCHED — libexpat $VER is >= $VULN_THRESHOLD"
exit 0
else
echo "VULNERABLE — libexpat $VER is < $VULN_THRESHOLD (CVE-2022-43680)"
exit 1
fiIf you remember one thing.
Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.