ImageMagick CVE-2026-116155 Crafted XMP Profile Memory Leak

NOTE

Responsible disclosure. Agilehunt discovered this vulnerability during source-level security research on ImageMagick and reported it to the maintainers. It is tracked as CVE-2026-116155 (GHSA-5jf2-9gcp-q234) and is fixed in 7.1.2-33 and 6.9.13-58. Details are shared for defensive and educational purposes only.

Vulnerability Description

ParseInternalDoctype (MagickCore/xml-tree.c) stores parameter-entity declarations from an internal DOCTYPE subset into a local predefined_entities vector. Each <!ENTITY % name "value"> declaration causes ParseEntities (xml-tree.c:1200) to call ConstantString (string.c:688) → AcquireQuantumMemory → malloc, producing an owning heap allocation stored in each odd-numbered slot of the vector.

At line 1565 the function relinquishes the pointer vector itself but never destroys the owned odd-slot value strings. The names (even slots) are non-owning pointers into the caller's UTF-8 buffer, but the values are independent heap allocations with no other reference. The local vector is never attached to XMLTreeRoot, so DestroyXMLTreeRoot — which frees only root->entities values — cannot reclaim them, and no later code path frees them either.

// xml-tree.c:1416 — each parameter-entity value is an owning heap allocation
entities[i+1] = ParseEntities(v, predefined_entities, '%');

// xml-tree.c:1565 — the vector is freed, the owned values are leaked
predefined_entities = RelinquishMagickMemory(predefined_entities);
return(MagickTrue);   // entities[1], entities[3], entities[5], ... leaked

Every parameter-entity value parsed from the DOCTYPE is therefore leaked on both the success path (line 1565) and the error paths that release the vector without destroying the values.

Attack path (verified in source and at runtime)

Stage Location Behavior
1 MagickCore/constitute.c:719 ReadImage obtains the registered decoder, checks coder authorization, invokes it
2 coders/miff.c:1197 ReadMIFFImage reads the attacker-declared profile length and exact bytes, passes them to SetImageProfilePrivate
3 MagickCore/profile.c:2045 SetImageProfileInternal checks profile size (default limit SSIZE_MAX), does opt-in libxml2 validation only when xmp:validate is true, and retains the XMP profile
4 MagickCore/constitute.c:842-866 ReadImage iterates decoded images and unconditionally requests GetImageProperty(next,"xmp:*") for each
5 MagickCore/property.c:1873 GetXMPProperty scans to the first literal <x marker and calls NewXMLTree there
6 MagickCore/xml-tree.c:1899-1920 NewXMLTree accepts an internal DOCTYPE regardless of perceived root state and calls ParseInternalDoctype
7 MagickCore/xml-tree.c:1416 ParseEntities returns an owning heap allocation stored only in an odd slot of the local vector
8 MagickCore/xml-tree.c:1565 the vector is relinquished without destroying its owned values — the final references are lost

Trigger conditions (all defaults unless noted): a build with MAGICKCORE_XML_DELEGATE (standard in default builds); an input coder that retains an XMP profile (MIFF, PNG, TIFF, JPEG, etc.); a profile of at least 17 bytes containing a <x marker before the DOCTYPE; a parser-visible internal DOCTYPE with one or more quoted parameter-entity declarations; and xmp:validate unset or false (the default).

Risk Breakdown

Field Value
CVE CVE-2026-116155
Advisory GHSA-5jf2-9gcp-q234
Risk Medium
CWE CWE-401 (Missing Release of Memory after Effective Lifetime), CWE-400 (Uncontrolled Resource Consumption)
CVSS 3.1 5.3 — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Affected ImageMagick 7.x before 7.1.2-33 and 6.x before 6.9.13-58 (with MAGICKCORE_XML_DELEGATE)
Affected Component ParseInternalDoctype — MagickCore/xml-tree.c:1416-1565, sink at line 1565
Fixed In 7.1.2-33 and 6.9.13-58
Reported by Agilehunt (VikzSharma)

Steps to Reproduce

Reproduced on ImageMagick 7.1.2-33 (Beta) Q16 x86_64, git commit 7656865 (2026-09-27, current main at time of report). Ubuntu 24.04.1 LTS, clang build, Q16 / non-HDRI / static with the XML delegate and AddressSanitizer + LeakSanitizer.

Step 1 — Generate the malicious XMP payload

A minimal internal DOCTYPE with ten parameter entities, each carrying a 1000-byte value:

n, vs = 10, 1000
xmp = '<x><!DOCTYPE r ['
for i in range(n):
    xmp += '<!ENTITY % leak' + str(i) + ' "' + 'B' * vs + '">'
xmp += ']><r/></x>'
open('payload.xmp', 'w').write(xmp)

The <x satisfies GetXMPProperty's marker scan (property.c:1887). The <!DOCTYPE r [...]> with an internal subset triggers ParseInternalDoctype via NewXMLTree (xml-tree.c:1920). Each <!ENTITY % name "value"> produces one leaked heap allocation.

Step 2 — Embed in a MIFF image and read it

ASAN_OPTIONS=detect_leaks=1 \
  magick convert -size 1x1 xc:red -profile xmp:payload.xmp poc.miff
ASAN_OPTIONS=detect_leaks=1 magick identify poc.miff

Step 3 — LeakSanitizer output (deterministic, 3/3 runs)

==2327499==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 10010 byte(s) in 10 object(s) allocated from:
    #3 ConstantString             MagickCore/string.c:688
    #4 ParseEntities              MagickCore/xml-tree.c:1200
    #5 ParseInternalDoctype       MagickCore/xml-tree.c:1416
    #6 NewXMLTree                 MagickCore/xml-tree.c:1920
    #7 GetXMPProperty             MagickCore/property.c:1887
    #9 ReadImage                  MagickCore/constitute.c:866

SUMMARY: AddressSanitizer: 10010 byte(s) leaked in 10 allocation(s).

Ten blocks, one per <!ENTITY %> declaration, each 1001 bytes (1000 B characters + NUL terminator). The leaked pointers were verified to contain the attacker-supplied entity values.

Control test: the identical image with only general <!ENTITY name "value"> declarations — which are correctly attached to root->entities and freed by DestroyXMLTreeRoot — produces no LeakSanitizer error (exit 0), confirming the parameter-entity local vector is the sole leak source.

Valgrind cross-check on the release build with a 100-entity, 10 KB-per-value payload:

definitely lost: 200,200 bytes in 200 blocks
    by ParseEntities         (MagickCore/xml-tree.c:1200)
    by ParseInternalDoctype  (MagickCore/xml-tree.c:1416)
    by NewXMLTree            (MagickCore/xml-tree.c:1920)
    by GetXMPProperty        (MagickCore/property.c:1887)

Two leak records of 100 blocks each — one from ReadImage's automatic xmp:* expansion, one from the explicit %[xmp:*] property access. The leak fires twice per image read.


Impact

An attacker who can submit an image to any ImageMagick-based service permanently consumes server memory. The XMP profile size limit defaults to SSIZE_MAX (effectively unlimited), so each malicious image leaks approximately the full profile size in parameter-entity values, and the leak fires twice per read. Measured on a production-like (non-sanitizer, glibc) build:

Scenario RSS Permanently leaked
Baseline (no XMP) 19 MB —
Single read (10 MB XMP profile) 58 MB ~39 MB
50 reads, one process 566 MB ~547 MB
500 reads ~5.5 GB sufficient to OOM-kill a 4–8 GB server

Each read leaks roughly 1.1× the XMP profile size, and memory is never reclaimed within the process lifetime. The attack requires no authentication, no user interaction, and no special privileges — only the ability to upload an image to a service that processes it with ImageMagick. External entity declarations (<!ENTITY % name SYSTEM "...">) are skipped by the parser, so the leaked values are always attacker-supplied strings: this is a pure availability attack, not information disclosure.

NOTE

The published advisory scores this Medium (CVSS 3.1 5.3, A:L). Our own analysis of long-running in-process deployments — where an unbounded profile size drives the leak toward OOM — argues the availability impact can reach High (A:H, 7.5) in practice. We cite the official advisory score above and flag the operational risk here.


Recommendation

  1. Destroy the owned values before freeing the vector (root cause). In ParseInternalDoctype, destroy the odd-slot owned allocations before relinquishing predefined_entities at xml-tree.c:1565:

    for (i=0; predefined_entities[i] != (char *) NULL; i++)
      if ((i & 1) != 0)  /* odd slots are owned allocations */
        predefined_entities[i]=DestroyString(predefined_entities[i]);
    predefined_entities=(char **) RelinquishMagickMemory(predefined_entities);
    
  2. Fix the error paths too. The circular-entity-declaration error at xml-tree.c:1425-1432 releases the vector after destroying only the current entry's value, leaving earlier entries leaked. Apply the same cleanup there.

  3. Upgrade. Move to ImageMagick 7.1.2-33 or 6.9.13-58, which contain the fix.

  4. Stopgap for operators (XMP vector only). Adding <policy domain="profile" rights="none" pattern="xmp"/> to policy.xml rejects the XMP profile at SetImageProfileInternal (profile.c:2046) and stops the leak through the XMP vector. This does not resolve the underlying defect — any code path that passes XML with an internal DOCTYPE containing parameter entities to NewXMLTree reaches the same sink — but it is a usable mitigation until you can patch.


References

TIMELINES

  • Sep 27, 2026 — Vulnerability discovered during source-level audit of xml-tree.c and confirmed with ASan and valgrind on ImageMagick 7.1.2-33 (commit 7656865)
  • Oct 10, 2026 — Security advisory GHSA-5jf2-9gcp-q234 published; fixed in 7.1.2-33 and 6.9.13-58
  • Oct 25, 2026 — CVE-2026-116155 assigned and published