Oct 6, 2026·8 min read·4 visits
A Use-After-Free vulnerability in librsvg (CVE-2026-96889) allows remote code execution when processing crafted SVG files with nested XML Inclusions (XInclude) and duplicate entity declarations, transitively impacting sharp, Next.js, and Ghost.
VectorFreed identifies a critical Use-After-Free (UAF) memory corruption vulnerability in librsvg (CVE-2026-96889), which manifests when parsing structured SVG documents containing nested XML inclusions (XIncludes) and duplicate entity declarations. The flaw results from an entity ownership conflict where librsvg prematurely deallocates an xmlEntity structure still actively referenced by the underlying libxml2 parser context. When transitively compiled into downstream applications such as the high-performance sharp image processing library, this vulnerability facilitates denial of service and unauthenticated remote code execution on the host operating system.
The open-source library librsvg is responsible for rendering SVG vector graphics into raster images. It functions as a critical component in server-side image processing pipelines. Many higher-level application runtimes, including Node.js, depend transitively on librsvg through image manipulation libraries such as sharp and its bundled dependency libvips. This widespread dependency structure extends the attack surface of vector image parsing to web backends, serverless functions, and content management platforms.
To parse raw XML and SVG structures, librsvg interfaces directly with libxml2, a C-based XML parsing library. The integration involves managing shared parser states, custom input/output callbacks, and the lifecycle of XML entities. When an application processes an SVG document, the parsing engine must resolve document structures, including XML entities and nested schemas. This complex processing environment necessitates precise synchronization of memory boundaries between the Rust abstractions of librsvg and the raw C pointers of libxml2.
The vulnerability, classified as CWE-416 (Use-After-Free), originates in this synchronization layer. When a document invokes nested XML Inclusions (XIncludes) that contain duplicate entity declarations, the memory ownership model breaks. The high-level library frees a memory segment that the low-level parser still references as an active pointer. This misalignment introduces a dangling pointer, allowing attackers to manipulate the heap layout and execute arbitrary system commands.
The root cause of the Use-After-Free flaw lies in the handling of XML entity registrations within shared parser contexts. During normal document parsing, libxml2 allocates a 144-byte structure known as xmlEntity to store entity names, types, and replacement values. The parser maintains a lookup table of these structures. When the outer document defines an entity, libxml2 populates this memory structure and maps it to the specified identifier.
When the parser encounters an entity reference like &active;, it begins expanding the replacement text by calling xmlCtxtParseEntity(). If this replacement text includes an external reference using the XML Inclusions (XInclude) specification, librsvg resolves the resource. It initiates a secondary parser context to interpret the target file, which may be dynamically retrieved or embedded as a base64-encoded data URL. To coordinate entity lookup, both the outer parser and the inner parser reference a shared XmlState entity map managed on the Rust side of librsvg.
If the nested document declares an entity with the exact same name as the outer entity (e.g., <!ENTITY active "R">), librsvg processes this duplicate declaration inside its entity insertion function entity_insert(). The insertion logic replaces the pre-existing outer entity map entry with the newly parsed nested entity pointer. This replacement triggers the destructor for the old entry, which calls xmlFreeNode() on the old xmlEntity pointer. Because libxml2's outer parser is still mid-execution within xmlCtxtParseEntity(), it retains a reference to this deallocated memory, resulting in a dangling pointer.
The vulnerability is located in the interaction between librsvg's entity map insertion and libxml2's reference management. Prior to the patch, the insertion logic did not verify whether the entity being replaced was currently in use by an active parser context. The following pseudocode illustrates the vulnerable implementation within librsvg:
// Vulnerable implementation in librsvg (before patch)
fn entity_insert(state: &mut XmlState, name: &str, entity: *mut xmlEntity) {
// The map unconditionally replaces the existing entity and takes ownership of the old pointer
if let Some(old_entity) = state.entities.insert(name.to_string(), entity) {
// Immediately frees the libxml2 node, even if the parent parser is actively referencing it
unsafe { xmlFreeNode(old_entity); }
}
}The upstream patch resolved this issue in commit 8a1b0cd319e9af2d1e9cf878081dd77f227a0504 by restructuring the insertion logic. Rather than unconditionally overwriting and deallocating the existing node, the library now detects duplicate keys within the active parsing context. If a duplicate entity name is registered, the incoming nested entity is safely discarded, or its deallocation is deferred, preserving the integrity of the active outer entity pointer:
// Patched implementation in librsvg (commit 8a1b0cd319e9af2d1e9cf878081dd77f227a0504)
fn entity_insert(state: &mut XmlState, name: &str, entity: *mut xmlEntity) {
// Check if the entity name is already present in the active state map
if state.entities.contains_key(name) {
// Discard the duplicate incoming entity node to prevent dangling pointers in the active parser
unsafe { xmlFreeNode(entity); }
return;
}
state.entities.insert(name.to_string(), entity);
}This defensive approach is complete because it maintains the invariant that no entity structure can be freed while an active parser execution stack holds its pointer. By prioritizing the preservation of the original entity definition over nested redefinitions, the parser context remains stable throughout its entire lifecycle.
Exploitation requires providing a crafted SVG document to an application that processes vector images using a vulnerable version of librsvg. The attack sequence involves defining an outer entity that contains an xi:include element. This element references a secondary, nested XML document encoded as a base64 data URL. The nested document must declare a duplicate entity using the same identifier as the outer entity.
When the outer parser processes &active;, it opens the nested SVG, triggering the duplicate entity insertion. This action deallocates the xmlEntity memory space. To achieve arbitrary code execution, the attacker must groom the heap. This is accomplished by allocating structured data into the newly freed 144-byte chunk before the outer parser regains execution control and attempts to access the deallocated structure.
On systems utilizing the glibc memory allocator, this is executed by sending carefully sized text strings or metadata fields immediately following the inclusion call. If the attacker successfully reclaims the chunk, they can overwrite the function pointers or internal string references of the xmlEntity structure. When the outer parser subsequently attempts to read or execute functions on the object, control flow is redirected to attacker-controlled memory regions.
The primary impact of this vulnerability is remote code execution (RCE) on the host operating system. The vulnerability is rated with a CVSS v3.1 score of 7.8 (High), with a vector of CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H. While the base CVSS rating assumes a local attack vector requiring user interaction, the practical risk is elevated in server-side deployments where applications automatically process user-supplied assets.
In the Node.js ecosystem, the vulnerability is highly exploitable due to downstream dependencies. The popular library sharp bundled the vulnerable librsvg statically in its prebuilt binary packages. Downstream platforms such as Next.js and Ghost exposed this code path to remote, unauthenticated users. In Next.js (CVE-2026-94545), a text-escaping vulnerability in the Satori library (GHSA-wx4j-mvgx-mqwp) allowed attackers to inject raw XML tags into the ImageResponse component, bypassing sanitization and feeding the malicious markup directly to sharp.
Similarly, Ghost (CVE-2026-105642) allowed users to create bookmark cards that fetched external web metadata. By serving a malicious SVG from a controlled web server, attackers could trigger the UAF inside Ghost's server-side image renderer. These examples demonstrate how a library-level memory corruption bug can scale into a remote code execution vector in modern web architectures.
The primary remediation path is upgrading all affected components to their patched versions. For applications directly utilizing the sharp npm package, update the dependency to version 0.35.5 or later. This version incorporates @img/sharp-libvips version 1.3.4, which statically links against the secure librsvg release version 2.63.2.
If the host operating system provides the librsvg library dynamically, administrators must apply package updates to ensure the system library is upgraded. The upstream vendor has backported security fixes to older stable release branches. The verified safe versions are: 2.63.2, 2.62.4, 2.61.5, 2.60.3, 2.57.5, and 2.56.6.
If immediate patching is not feasible, administrators can block SVG parsing entirely within sharp by disabling the SVG loader module. This mitigation prevents the vulnerable parsing library from executing while maintaining functionality for raster image processing:
const sharp = require('sharp');
sharp.block({ operation: ["VipsForeignLoadSvg"] });Additionally, ensure that the Node.js runtime and host binaries are compiled with Position Independent Executable (PIE) and Address Space Layout Randomization (ASLR) enabled. Official prebuilt binaries distributed directly from nodejs.org may lack these hardening flags, whereas standard package manager distributions on enterprise Linux operating systems typically include them, reducing the likelihood of reliable exploitation.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
librsvg GNOME | < 2.63.2 | 2.63.2 |
sharp Lovell Fuller | < 0.35.5 | 0.35.5 |
Next.js Vercel | >= 16.2.0, <= 16.3.5 | 16.3.6 |
Ghost TryGhost | >= 6.56.0, < 6.67.0 | 6.67.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-416 |
| Attack Vector | Local / Network via Transitive Library Calls |
| CVSS Base Score | 7.8 (High) |
| EPSS Score | 0.00129 (Percentile: 2.15%) |
| Exploit Status | Proof-of-Concept Available |
| CISA KEV Status | Not Listed |
The product uses a pointer that is associated with a pre-existing resource after the resource has been freed, leading to memory corruption and potential code execution.
An Improper Access Control vulnerability (CWE-284) in Payload CMS prior to version 3.90.0 and 4.0.0-canary.34 allows authenticated, low-privileged users to bypass field-level access control restrictions and overwrite the password of other accounts, leading to complete account takeover and privilege escalation.
Payload CMS was discovered to use an insecure default configuration for its password-hashing mechanism. The system requested a 512-byte key from PBKDF2-HMAC-SHA256 with 25,000 iterations, creating a severe cryptographic asymmetry. While the defending server sequentially computed 16 blocks of key material (equivalent to 400,000 internal iterations), an offline attacker only needed to compute the first 32-byte block to verify password guesses. This allowed offline attackers to crack stolen database hashes 16 times faster than intended by the security design.
CVE-2026-102275 (GHSA-x33g-cr3x-6449) is a public/private key identity confusion vulnerability in PyJWT versions 2.1.0 through 2.14.0. When importing Octet Key Pair (OKP) JSON Web Keys (JWKs) representing Ed25519 or Ed448 curves, PyJWT fails to verify that the public parameter 'x' matches the private parameter 'd'. An attacker can construct a hybrid JWK combining a victim's public key with the attacker's private key. In protocols like DPoP that bind sessions via public key thumbprints, this allows the attacker to authenticate as the victim while signing proofs with their own private key, fully bypassing sender-constrained security guarantees.
An authentication bypass and privilege escalation vulnerability exists in Filament (filamentphp/filament) due to missing password verification during multi-factor authentication (MFA) setup and management. An attacker with access to an active session can modify or disable MFA, leading to account hijacking.
A critical Denial of Service (DoS) vulnerability exists in @socket.io/cluster-engine before version 0.1.1. Unauthenticated remote attackers can crash the server process by supplying inherited prototype property names as session identifiers.
A vulnerability in the Client-Side Field-Level Encryption (CSFLE) component of the MongoDB Python Driver (PyMongo) allows an attacker with database write access to trigger local Unix domain socket connections. By manipulating the Key Management Service (KMS) endpoint configuration inside the key vault collection to end with a '.sock' extension, an attacker forces the application to perform a Server-Side Request Forgery (SSRF) against internal Unix domain sockets.