Operating system, libraries and runtimes
libheif: HEIF/AVIF decoder flaw
libheif HEIF/AVIF Decoder Check
A heap buffer overflow in libheif, the HEIF and AVIF image decoder, was used to get remote code execution on a Discourse forum and then pivot further into the services behind it. The decode path is reached by uploading a crafted HEIF or AVIF image: an app that processes user images hands the file to ImageMagick (or libvips), which calls libheif. Disabling a single "image uploads" setting does not reliably close it, because avatars, emoji, oneboxed remote images and other surfaces feed the same conversion path.
The fix is to update libheif (and libde265, the HEVC codec it calls) and rebuild anything that bundles them. For a Discourse Docker install this means a container rebuild, not a web-interface update, because the old library stays in the image until it is rebuilt:
cd /var/discourse
git pull
./launcher rebuild app
Fixed-version information varies across upstream releases and distribution packages. The upstream heap-write class was fixed across 1.21.0, 1.22.0 and 1.22.1, with security releases continuing into the 1.23.x line. Debian fixed a batch in DSA-6417-1 (libheif 1.19.8-1+deb13u1, trixie, 8 August 2026), and other distributions backport under their own version strings, so a package can carry the fixes under an older-looking upstream number. This script accounts for that: a distribution build is judged by FIXED_BUILDS and its package changelog first, and only falls back to the upstream version floor when it is not a distribution package.
This is a separate issue from the Ruby Regexp CVEs. For Ruby and GitLab, use ruby/ruby-regexp-oct2026-check.sh instead.
libheif quick start
-
Download the script. The repository is mirrored on GitHub and Codeberg; pick a base URL.
# BASE from GitHub: https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main # BASE from Codeberg: https://codeberg.org/PrivacySafe/security-patches/raw/branch/main # curl -fLO "$BASE/heif/libheif-oct2026-check.sh" curl -fLO https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main/heif/libheif-oct2026-check.sh -
Read it before running it. Always.
less libheif-oct2026-check.sh -
Make it executable.
chmod +x libheif-oct2026-check.sh -
See what's exposed. Root is optional, but without it you won't read every library or exec into other users' containers.
sudo ./libheif-oct2026-check.sh check -
Check whether ImageMagick still lets HEIF through.
policyreports and prints the lines to block it.hardenwrites them for you (reversible withunharden).sudo ./libheif-oct2026-check.sh policy sudo ./libheif-oct2026-check.sh harden -
Look for crashes that suggest someone already tried.
sudo ./libheif-oct2026-check.sh hunt
To confirm GitHub and Codeberg serve the same file, compare checksums:
sha256sum libheif-oct2026-check.sh
curl -fsSL https://codeberg.org/PrivacySafe/security-patches/raw/branch/main/heif/libheif-oct2026-check.sh | sha256sum
libheif commands
| Command | What it does |
|---|---|
check (default) |
Reports every libheif and libde265 on the host, the ImageMagick and libvips that reach them, every Discourse install, and libheif inside running containers, with whether each libheif carries the fixes. For a Discourse container it says a rebuild is needed, not a restart. Changes nothing, root optional. Exits 0 if nothing is exposed, 1 if a vulnerable libheif is reachable, 2 if something couldn't be judged |
policy |
Read-only. Checks whether ImageMagick's policy.xml still lets HEIC, HEIF or AVIF through, and prints the lines that block them. Exits 0 if HEIF is blocked everywhere a policy was found, 1 if it is still allowed, 2 if no policy was found |
harden |
Writes a managed block into ImageMagick's policy.xml that refuses the HEIC, HEIF and AVIF coders, so a crafted upload never reaches libheif through ImageMagick. Backs the file up first, is idempotent, and is the one thing this script changes. Needs root. Defense in depth, not a substitute for updating libheif |
unharden |
Removes the managed block and restores. Needs root. Only touches a block this script wrote; a block added by hand is left alone |
mw-harden |
Blocks HEIF, HEIC and AVIF uploads in every MediaWiki found, by adding those MIME types to $wgMimeTypeExclusions in LocalSettings.php, so a crafted upload is rejected by its detected type before the scaler runs. Backs the file up first, is idempotent, checks the result still parses as PHP before saving, and is reversible. Needs root. Application-level defense, not a substitute for updating libheif |
mw-unharden |
Removes the MediaWiki block and restores. Needs root. Only touches a block this script wrote |
hunt |
Read-only log triage for image-processor crashes: ImageMagick, libheif and libde265 segfaults, HEIF decoder frames in backtraces, services killed by a crash signal, and repeated HEIF decode errors. Exits 0 if nothing matched, 1 if it found something, 2 if no journal or log could be read (nothing was searched) |
version |
Prints the script version. Every other command prints it on its first line too |
What the libheif check looks at
libheifon the host. Everylibheifshared object on the standard library paths and inldconfig's cache, judged by its package build or, if it is not a distribution package, by the upstream version floor. The version is read from the library's own soname, never from a stray build string inside it, and the library is never executed.libde265. The HEVC codeclibheifcalls for HEIC. It has its own CVE history and its own fixed versions, so the script reports its version but does not judge it against thelibheiffloor. Update it from your distribution too.- What reaches
libheif. ImageMagick (magickorconvert) and libvips, with whether ImageMagick can even decode HEIF or AVIF. A build with no HEIF delegate is not a path to this bug through that tool. - Discourse. The Docker launcher under
/var/discourse, a bare-metal checkout under/var/www/discourse, anddiscourse/discourseanddiscourse/baseimages. A forum's image-upload path runs an attacker-supplied file through ImageMagick intolibheif, so a Discourse install on top of a vulnerablelibheifis a path to it. For a Docker install the library lives inside the web container, so the script points at the container section and tells you to rebuild rather than restart. - MediaWiki.
LocalSettings.phpunder the usual web roots. MediaWiki is a path to this bug only when it is configured for it, so the script reads the config rather than assuming. The scaler must be ImageMagick ($wgUseImageMagick), not GD, which is the default when both are installed. A HEIF-family extension must also be in$wgFileExtensions, and none is by default. It reports a wiki as exposed only when both are true, and otherwise says why it is not a path. Reading the PHP config file is not executing it. libheifinside running containers. For each running Docker or Podman container, the script reads thelibheifthe container actually loads. This is how a Discourse web container still carrying the old library gets caught: a web-interface update does not replace it, only a rebuild does.
libheif patch detection
For each libheif the checks run in order, and the first that applies decides:
FIXED_BUILDS. Distribution builds you list as fixed, for a backport the script doesn't know about. A build only counts for the same upstream version.- Changelog. For a
libheifinstalled from a Debian or Ubuntu package, the package changelog names DSA-6417-1 or one of the batch CVEs, and its newest entry is the installed build. Because distributions backport under old upstream numbers, this overrides the version floor: a package at1.17.6can read as fixed when its changelog proves it. - Upstream floor. For a library that is not a distribution package (self-built or bundled), the version is at or above
HEIF_FIXED_UPSTREAM(default 1.22.0). A dpkg-owned build with no changelog proof is reported as unknown rather than fixed, because its number alone cannot clear it.
libheif disabling the coder as defense in depth
Even after libheif is patched, more decoder bugs in this format are likely, so it is worth refusing untrusted HEIF and AVIF at the converter. policy reports whether ImageMagick already does, and prints the lines to add inside <policymap> in policy.xml:
<policy domain="coder" rights="none" pattern="HEIC" />
<policy domain="coder" rights="none" pattern="HEIF" />
<policy domain="coder" rights="none" pattern="AVIF" />
harden writes those lines for you, as a marked, managed block, so you don't have to edit the XML by hand:
sudo ./libheif-oct2026-check.sh harden # write the block (backs the file up first)
sudo ./libheif-oct2026-check.sh unharden # take it back out
harden is the only host-wide change this script makes. It backs the file up under /var/backups/libheif-oct2026/ first, writes one managed block (running it again refreshes that block rather than adding a second), and creates a policy.xml if none exists, since ImageMagick with no policy decodes HEIF by default. unharden removes only the block this script wrote and leaves any coder lines you added by hand alone. This stops ImageMagick handing HEIF and AVIF to libheif. It is not a substitute for updating the library, and it does not help a caller that reaches libheif without going through ImageMagick. Discourse added ImageMagick sandboxing as its own defense in depth.
libheif blocking HEIF uploads in MediaWiki
The ImageMagick coder policy above is host-wide. If you run a MediaWiki that is configured to accept HEIF (ImageMagick scaler plus a HEIF-family extension in $wgFileExtensions), mw-harden blocks the upload at the application instead, which is more targeted and survives an ImageMagick reconfigure:
sudo ./libheif-oct2026-check.sh mw-harden # block HEIF/HEIC/AVIF uploads in every wiki found
sudo ./libheif-oct2026-check.sh mw-unharden # take the block back out
It appends a marked, managed block to LocalSettings.php. The block adds the HEIF-family MIME types (image/heif, image/heic, image/avif, and the -sequence variants) to $wgMimeTypeExclusions, mirrors that to the pre-1.37 $wgMimeTypeBlacklist name when it is in use, and drops heif, heic and avif from $wgFileExtensions if they are there. Excluding by MIME type rejects a crafted file by its detected type, so renaming it to .jpg does not get it past. The edit is backed up first, is idempotent, and is checked with php -l before it is saved, so a file that would not parse is left untouched (and its backup kept). This is an application-level block. The host libheif still needs updating.
libheif hunting for exploitation
A crafted image corrupts memory while it decodes, so a failed attempt usually shows as a crashed image worker rather than an application error. hunt reads the journal and logs, never changes anything, and reports ImageMagick, libheif and libde265 segfaults, libheif and libde265 frames in a backtrace, services killed by a crash signal, heap corruption aborts, and repeated HEIF, HEIC or AVIF handling errors. HUNT_SINCE sets how far back it reads the journal, and HUNT_LOGS adds your own log files.
A match tells you exactly where to look. Image decoders crash on genuinely broken files too. A crash with libheif or libde265 frames on a vulnerable build, or a burst of HEIF decode errors from one source just before a worker died, is worth treating as an incident: check what the process could reach and rotate it. On a forum that includes its admin and session stores.
libheif configuration
| Variable | Default | Purpose |
|---|---|---|
HEIF_FIXED_UPSTREAM |
1.22.0 |
Upstream floor the heap-write class is fixed at, used only for libraries that are not distribution packages |
FIXED_BUILDS |
(empty) | Extra fixed distribution builds, space-separated full versions |
DISCOURSE_ROOT |
/var/discourse |
Where the Discourse Docker launcher lives |
DISCOURSE_APP |
/var/www/discourse |
Where a bare-metal Discourse checkout lives |
POLICY_PATHS |
ImageMagick-6 and -7 config dirs | Where policy looks for policy.xml |
MW_PATHS |
common web roots | Where check and mw-harden look for MediaWiki's LocalSettings.php |
HUNT_SINCE |
2026-07-01 |
How far back hunt reads the journal |
HUNT_LOGS |
(empty) | Extra log globs for hunt |
Before you run it
- The real fix is the update, which this script does not run for you. Update
libheifandlibde265, rebuild anything that bundles them, and for Discourse rebuild the container (/var/discourse/launcher rebuild app), which a web-interface update does not do. A package upgrade or a container rebuild is an administrator's call to time, so the script prints it rather than firing it. - It changes only two things, and only when asked.
hardenwrites the ImageMagick coder block.mw-hardenwrites the MediaWiki upload block. Both back the file up first and are reversible (unharden,mw-unharden). Everything else is read-only. A single upload setting is not the fix on its own, because many surfaces reach the decoder. Blocking the coder at the converter (or the upload at the wiki) is a reliable protective step while you schedule the update. - MediaWiki is only exposed when configured for it. The check flags a wiki only when it uses ImageMagick and accepts HEIF. A default install uses GD and does not accept HEIF, so it is not a path.
mw-hardeneditsLocalSettings.php, so it checks the result parses withphp -lbefore saving and leaves a file that wouldn't parse untouched. - Distribution backports read correctly. A package can carry the fixes under an older upstream number. The script trusts the package changelog over the version. If you know a build is fixed and the changelog doesn't name it, add it to
FIXED_BUILDS. libde265is reported, not judged. It has its own fixed versions. Update it from your distribution.- Containers need their own fix. Each image carries its own
libheif. Rebuild on a fixed base. The script reads the library inside running containers but can't see one that was installed in an unusual way. - GNU/Linux only. Library discovery and container inspection assume it.
- Every backup has its own private directory.
harden,unharden,mw-hardenandmw-unhardenkeep the file they are about to change in a new directory underBACKUP_ROOT(mode0700), so two runs in the same second cannot overwrite the original, and aLocalSettings.phpbackup, which holds database credentials, is not world-readable.
The Security Patches software and this documentation are dedicated to the public domain under CC0 1.0 Universal. Other pages on privacysafe.app/docs are CC BY-SA 4.0 unless the page says otherwise.