Engagement Report: Image Payload Injection Against Four Sanitization Pipelines

A redacted-format sample of what an engagement deliverable looks like: threat model, findings ranked by severity, reproduction method, and mitigations, run fresh against current ImageMagick and Pillow rather than quoted from an old result.

This is a sample engagement report, built on my own open tool, Image Payload Injection, rather than on a client's name. The format is the one I'd hand a client: threat model, findings ranked by severity, how each was reproduced, and what to do about it. Everything below is a real, current result, run the morning this was published against today's ImageMagick (7.1.2) and Pillow (11.3.0), not carried over from an older write-up.

Payload source and exact reproduction code stay in the GitHub repo, under its own authorized-research framing. This report describes what the tests found and what they mean, the way a client deliverable would.

Scope and Objective

Target: an image-ingestion pipeline that accepts user-supplied PNGs and runs them through a sanitization step before the image reaches a downstream consumer, in this case a vision-language model's context window.

Objective: determine which classes of injected payload survive each of four common sanitization strategies, and rank the surviving classes by how hard they are to detect and stop.

Out of scope: the VLM's own prompt-injection handling once a payload reaches its input (a separate engagement), and any pipeline not represented in the four tested below.

Threat Model

An attacker who can get an image into the pipeline (upload, scrape, shared link) wants one of three outcomes: inject instructions a downstream model will read as part of its prompt, exfiltrate data through a channel that bypasses visual review, or evade a content-moderation classifier that only looks at pixels. None of these require compromising the pipeline itself, only surviving whatever sanitization sits in front of it.

Four sanitization pipelines were tested, chosen because they represent what most teams actually run, not a hardened reference implementation:

| Pipeline | What it does | | --- | --- | | imagemagick_strip | convert -strip, the one-line fix most teams reach for first | | imagemagick_full | strip plus an explicit chunk-exclusion list for tEXt/iTXt/zTXt/iCCP | | pillow_reopen | decode and re-encode through Pillow, metadata untouched | | pillow_reopen_no_exif | decode and re-encode through Pillow with EXIF explicitly dropped |

Seven payload techniques were tested against all four: EXIF UserComment injection, PNG tEXt/iTXt chunk injection, a custom polyglot chunk, invisible-contrast text, micro-typography tiling, channel-isolated text, and adversarial pixel perturbation.

Findings, Ranked by Severity

Critical: Adversarial pixel perturbation survives every pipeline tested

All four sanitization pipelines passed this payload through unchanged. The reason is structural, not a configuration gap: a perturbation crafted to shift a model's interpretation of the image is still composed of valid pixel values. Any sanitization step that preserves visual fidelity, which is the entire point of re-encoding instead of discarding the image, cannot distinguish a perturbed pixel from a legitimate one. Stripping metadata, re-encoding through ImageMagick, or round-tripping through Pillow all leave the pixel data, and the payload riding on it, intact.

This is the one finding in this report with no sanitization-side fix. It has to be caught upstream (input classification, ensemble disagreement between a vision model and a lighter-weight perturbation detector) or not at all.

Informational: all six metadata-level and typography-level techniques were caught by every pipeline tested

EXIF UserComment injection, PNG tEXt/iTXt chunk injection, the custom polyglot chunk, and all three invisible/channel-isolated typography variants were stripped cleanly by all four pipelines in this test run. This is a meaningfully different result than this same framework produced in earlier testing: on older ImageMagick and Pillow releases, tEXt chunks and EXIF UserComment data were documented to survive a plain -strip when the chunk-exclusion list wasn't set explicitly, and the polyglot chunk's survival depended on whether the downstream consumer parsed PNG chunks directly rather than through ImageMagick.

That gap has closed on the versions tested here. The practical implication for a team relying on this class of mitigation: re-verify it against your actual production dependency versions, not against a blog post or a vendor's documentation, because this exact survival matrix moved between library versions without any application code changing.

Reproduction

The test harness generates each payload programmatically against a clean baseline PNG, runs it through each pipeline, and checks the sanitized output for the original payload content. It is deterministic and re-runnable: the full matrix (7 techniques × 4 pipelines = 28 cells) completes in under a second on a current machine, which is what makes it practical to re-run on every dependency bump rather than trust a finding that may already be stale.

This is the actual invocation that produced the results above, run this morning against the repo's current main:

from ipi.sanitizer import RedBlueTester

with open("test_images/01_clean_baseline.png", "rb") as f:
    base_png = f.read()

tester = RedBlueTester()
matrix = tester.run_full_matrix(base_png)
print(tester.report_text(matrix))
# -> 4 critical bypasses, all `adversarial_perturbation`;
#    all 6 other techniques clean across all 4 pipelines.

run_full_matrix() builds all seven payloads internally and runs each through all four pipelines; report_text() renders the bypass table shown above. The payload generators themselves, the code that actually constructs the EXIF/chunk/pixel-level injections, are in the same class and stay in the repo rather than in this report: that's the dividing line between a reproducible test and a standalone exploit kit, and it's the one this report holds to throughout.

Mitigations

  1. For the metadata and typography classes: the tested pipelines already handle this correctly as configured. The risk isn't the technique, it's assuming a fix holds across a dependency upgrade without re-testing it. Pin sanitizer-library versions deliberately, and re-run a survival matrix like this one as part of the upgrade process, not after an incident.
  2. For adversarial perturbation: sanitization alone will not catch this class, on any pipeline, by construction. Mitigation has to move upstream of the sanitizer: a lightweight classifier trained to flag statistical anomalies consistent with adversarial perturbation, or disagreement-based detection (run the image through two independently-trained vision models and flag divergent outputs). Treat this as a detection problem, not a sanitization problem.
  3. For any pipeline not in this table: test it. The whole value of a result like this is that it's falsifiable and current for one specific configuration. A pipeline that looks similar on paper (a different ImageMagick flag, a different Pillow save path) is a different pipeline and needs its own run.

GhostInThePrompt.com // Re-run the test before you trust the old result. The versions moved and the finding moved with them.