Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## Version 1.3.2

### Security

* Ship a hardened ImageMagick `policy.xml` in the final image to mitigate image-decompression-bomb DoS via the Appwrite storage/avatars preview pipeline. A crafted image (small on disk, huge dimensions) previously decoded unbounded, spilling ImageMagick's pixel cache to disk and filling the volume — killing MongoDB or the container. The policy caps `disk` (4GiB spill limit — the primary control; sized for a 150MP Q16-HDRI photo) plus generous `width`/`height` backstops (50KP, well above any real camera so legitimate high-res photos are never rejected), sets `memory`/`map`/`area`/`thread` limits, and disables coders/modules never used for previews (PS/EPS/PDF/XPS/MSL/MVG/HTTP/etc., plus SVG/SVGZ/MSVG via a `module` deny that also closes the SVG SSRF/XXE delegate route). Installed into ImageMagick's configure dir (discovered at build time); the build fails if the policy does not load. Added a `tests.yaml` assertion verifying the policy is active.

## Version 1.3.1

### Fix
Expand Down
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,18 @@ RUN apk update && \
zstd-libs \
&& rm -rf /var/cache/apk/*

# Hardened ImageMagick policy — bounds per-decode width/height/disk so a crafted
# "image bomb" (small on disk, enormous when decoded) cannot exhaust memory or
# fill the disk and take down the container or its neighbours. Installed into
# ImageMagick's configure dir, discovered at build time so it survives package
# path changes; the build fails loudly if the policy does not load.
COPY policy.xml /tmp/policy.xml
RUN set -eux; \
POLICY_DIR="$(identify -list configure | awk '/^CONFIGURE_PATH/ {print $2}' | cut -d: -f1)"; \
cp /tmp/policy.xml "${POLICY_DIR%/}/policy.xml"; \
rm /tmp/policy.xml; \
identify -list policy | grep -q '50KP'

WORKDIR /usr/src/code

COPY --from=core-extensions /artifacts/ /tmp/exts/
Expand Down
73 changes: 73 additions & 0 deletions policy.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policymap [
<!ELEMENT policymap (policy)*>
<!ATTLIST policymap xmlns CDATA #FIXED "">
<!ELEMENT policy EMPTY>
<!ATTLIST policy xmlns CDATA #FIXED "">
<!ATTLIST policy domain NMTOKEN #REQUIRED>
<!ATTLIST policy name NMTOKEN #IMPLIED>
<!ATTLIST policy pattern CDATA #IMPLIED>
<!ATTLIST policy rights NMTOKEN #IMPLIED>
<!ATTLIST policy stealth NMTOKEN #IMPLIED>
<!ATTLIST policy value CDATA #IMPLIED>
]>
<!--
Hardened ImageMagick security policy for Appwrite.

Bounds the resources a single decode may consume so a crafted image (small
on disk, enormous when decoded - an "image bomb") cannot exhaust memory or
fill the disk and take down the container or its neighbours.

Only width, height and disk raise an exception when exceeded, so they are the
real guards: width/height reject oversized dimensions before the raster is
allocated, and disk caps the pixel-cache spill. memory/map/area merely change
when the cache moves to disk. All values are tunable.

Preview inputs are limited to JPEG, PNG, HEIC, WEBP and GIF (output adds
AVIF), so the delegate coders disabled below are never used and only add risk.
Validate changes with https://imagemagick-secevaluator.doyensec.com/.
-->
<policymap>
<policy domain="Undefined" rights="none"/>

<!-- Hard limits: exceeding these throws an exception and stops processing.
disk is the real DoS control - it caps the pixel-cache spill so a bomb
cannot fill the volume. width/height are only fast-fail backstops against
absurd dimensions, set well above any real camera (a 200MP phone sensor
is ~16320px on its long side) so legitimate photos are never rejected.
ImageMagick here is Q16-HDRI (16 bytes/pixel), so a 150MP photo needs
~2.4GB of cache - hence the 4GiB disk headroom. -->
<policy domain="resource" name="width" value="50KP"/>
<policy domain="resource" name="height" value="50KP"/>
<policy domain="resource" name="disk" value="4GiB"/>
<policy domain="resource" name="list-length" value="128"/>
<policy domain="resource" name="time" value="120"/>

<!-- Soft limits: control when the pixel cache spills to memory-mapped disk.
area is generous enough to keep large phone photos in the mem/map path. -->
<policy domain="resource" name="memory" value="256MiB"/>
<policy domain="resource" name="map" value="512MiB"/>
<policy domain="resource" name="area" value="512MP"/>
<policy domain="resource" name="file" value="768"/>
<policy domain="resource" name="thread" value="2"/>

<!-- Cap a single allocation request (guards against a malicious header
claiming a huge buffer); generous so large legitimate photos decode. -->
<policy domain="system" name="max-memory-request" value="512MiB"/>

<!-- Disable coders/delegates Appwrite never uses for previews; these are the
classic ImageMagick RCE/SSRF vectors. -->
<policy domain="delegate" rights="none" pattern="HTTPS"/>
<policy domain="delegate" rights="none" pattern="HTTP"/>
<policy domain="coder" rights="none" pattern="{PS,PS2,PS3,EPS,EPI,EPT,EPSF,EPSI,PDF,XPS}"/>
<policy domain="coder" rights="none" pattern="{MSL,MVG,SVG,SVGZ,MSVG,TEXT,LABEL,CAPTION}"/>
<policy domain="coder" rights="none" pattern="{URL,FTP,EPHEMERAL,MPEG,MPG}"/>
Comment thread
greptile-apps[bot] marked this conversation as resolved.

<!-- SVG is an SSRF/XXE vector (its renderer resolves external hrefs and
entities) and is never a preview input. The module deny also closes the
delegate route (e.g. rsvg), which a plain coder deny does not. -->
<policy domain="module" rights="none" pattern="{SVG,MSVG,SVGZ}"/>

<!-- Block indirect reads (e.g. @file, caption:@file) that can leak local files. -->
<policy domain="path" rights="none" pattern="@*"/>
</policymap>
8 changes: 8 additions & 0 deletions tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ commandTests:
args: ["-i"]
expectedOutput:
- "ImageMagick supported formats .*WEBP.*"
- name: 'ImageMagick hardened policy loaded'
command: "identify"
args: ["-list", "policy"]
expectedOutput:
- "name: width"
- "name: height"
- "name: disk"
- "50KP"
- name: 'PHP intl'
command: "php"
args: ["-r", 'print(\Normalizer::FORM_D);']
Expand Down
Loading