Most prompt injection defenses guard the text prompt. They inspect the user's message, sometimes the retrieved documents, and they assume everything else arriving at the model is inert data. For a multimodal model that assumption is wrong. The model reads the image too, and an image is a large, high-entropy container that a human reviewer looks at for half a second and waves through.
You can hide instructions in that container. Three techniques are old enough to be boring. Least-significant-bit steganography writes the payload into the low bits of the pixel values. DCT-coefficient embedding writes it into the frequency domain so it survives JPEG compression. And the simplest one drops text into EXIF metadata, where nobody looks. None of these visibly change the image. All of them can carry a paragraph of instructions.
This stopped being speculative in 2026. CSA and IEEE both published systematic studies of image-based prompt injection this year, and their taxonomy has four branches: typographic text, steganographic encoding, adversarial pixel perturbation, and physical-world signage. The measured numbers are not marginal. Steganographic embedding reached 24.3% attack success across GPT-4V, Claude, and LLaVA, neural steganographic methods reached 31.8%, and stealth-constrained configurations went as high as 64%.
What that literature is short on is defenses. Almost all of it measures attacks. So I built the other half and measured it the same way.
The approach
crossguard-py sanitizes the image before it reaches the model. The design is deliberately unclever: rather than try to detect a payload, destroy the channel the payload lives in. Detection loses to an adaptive attacker, because the attacker gets to pick the encoding. Channel destruction does not care what the payload says or how it was encoded, only where it physically lives.
So the pipeline scrubs the low bit planes, forces the image through a recompression path that requantizes the DCT coefficients, and strips metadata to the studs rather than filtering it. The output is a new image that is visually close to the input and carries none of the original low-order structure.
That is Layer 1, and I consider it production ready.
What the numbers actually are
99.84% of hidden payloads destroyed across 108,015 samples, at a mean output SSIM of 0.925.
| technique | n | destroyed | SSIM |
|---|---|---|---|
| LSB | 60,005 | 99.88% | 0.939 |
| DCT | 24,005 | 99.57% | 0.904 |
| metadata | 24,005 | 100.0% | 0.909 |
| source | n | destroyed | SSIM |
|---|---|---|---|
| kaggle/stego (real LSB binaries) | 24,000 | 99.71% | 0.957 |
| kaggle/stego_b64 (real) | 6,000 | 99.93% | 0.964 |
| kaggle/stego_zip (real) | 6,000 | 100.0% | 0.965 |
| synth / ALASKA covers | 45,000 | 99.80% | 0.902 |
| synth / BOSSBase covers | 27,015 | 99.97% | 0.916 |
The part that matters more than the percentage is that the ground truth is verifiable rather than asserted. For the real stego binaries, the cover images have a zeroed LSB plane, which means a stego image's 1-bits literally are the payload. Destruction is then measured as residual payload correlation, P(after=1 | before=1) minus P(after=1 | before=0), below a 0.10 threshold. That metric is independent of the base rate, so it cannot be gamed by an image that happens to be mostly zeros. For the synthetic cases a known payload goes in and the check is whether that exact payload is still extractable afterward.
Per-sample results ship in the repository, so the headline number is recountable. You do not have to take 99.84% on faith. Roughly 1.0% of samples were unscored, either because a synthetic embed did not round-trip or because a real payload was too short to correlate, and those are excluded from the denominator rather than counted as successes.
Two things I got wrong
Both are worth stating because they are the reason I trust the current number.
The first was measuring on synthetic data. An early version looked excellent against payloads embedded in generated test images, and that result was hollow. On real photographic covers the DCT path failed 41% of the time, because a blur-only approach that comfortably destroys a payload in a smooth synthetic gradient does not destroy one hidden in the texture of a real photograph. Real covers are the only covers that count.
The second was an LSB residual tail. About 6% of cases retained enough correlation to be suspicious, which turned out to be a boundary condition in where the scrub was applied rather than a fundamental limit. The fix moved the guarantee to the output boundary, so the property is enforced on what actually leaves the function rather than on an intermediate buffer.
Neither of these would have surfaced without per-sample evidence to dig through.
What it costs
It is not visually lossless. Mean SSIM is 0.925 and it is lower on textured cover sets, around 0.90 on the ALASKA and BOSSBase DCT path. If you are sanitizing product photography where fine detail is the product, measure that tradeoff on your own images before you deploy this.
Single-image latency is roughly 110 to 145 milliseconds at 512 pixels, unloaded. The aggregate p95 in the benchmark is 332 milliseconds, but that number is inflated by 14-way CPU contention during the run and should not be used as an SLA.
Scope is LSB, DCT-coefficient, and EXIF metadata steganography. Adversarial pixel perturbations, the second branch of that CSA taxonomy, are not addressed and I would rather say so plainly than let a reader assume the coverage is broader than it is.
Layer 2, which is research and not a product
Typographic injection is the other branch I have worked on: instructions rendered as visible text inside the image, which a vision model reads as an instruction and a text guard never sees. The current pipeline OCRs the image, classifies the extracted text for harm, and either blanks the text regions or passes the image through.
Against 450 attacks per arm on Qwen2-VL with a HarmBench judge, spread across nine adaptive attack variants: undefended attack success 0.484, blanket redaction 0.000, and selective redaction at a 0.90 threshold 0.149, while preserving 89% utility on held-out real documents.
That 0.000 should be read as "below roughly 0.8% at 95% confidence" rather than as proven zero. And blanket redaction destroys document utility completely, so it is only a real option if you never need to read the document.
Three things keep this in the research column. It leaks about one attack in seven at the useful threshold. It runs at about 4.9 seconds per image, which is too slow to sit inline. And the weak spots are unfixed: curved text at 0.36 and severely rotated text at 0.32, though the Wilson intervals on those overlap enough that the per-variant table is not a ranking.
There is also a bug worth describing, because it is the kind that makes a defense report success while doing nothing. OCR ran on a deskewed copy of the image while redaction blanked the original. On rotated text the coordinates did not correspond, so the fill landed in the wrong place. The defense counted the boxes it had blanked, reported a healthy number, and left readable attack text on screen. It was faking the best metric I had. Splitting the geometric transform from the photometric one, so the frame that gets read is the frame that gets written, moved blanket redaction from a false 0.017 to a real 0.000.
Widening the adaptive attack set from six variants to nine moved selective redaction from 0.093 to 0.149. That is the honest cost of attacks I had not imagined when I designed the defense, and it is roughly 60% more leakage. I expect a fourth round would move it again.
One fix failed outright, and I am publishing it because negative results are load-bearing. The intuition was that garbled attack text should produce low OCR confidence, giving a cheap trigger for the fail-safe path. It does the opposite. EasyOCR reports mean confidence around 0.75 on scrambled adversarial glyphs and around 0.64 on genuinely noisy benign scans. It is confidently wrong on the attacks and hesitant on the real documents, so the catch rate at any usable threshold is zero. The gap is semantic, not a detection problem, and no confidence threshold will close it.
What I would actually recommend
If you are passing user-supplied images to a multimodal model, sanitize them at ingest. The steganographic channel is cheap to close, the evidence that it needs closing is now in the literature, and the tradeoff is a fidelity cost you can measure on your own data in an afternoon.
If you are worried about typographic injection, treat every available answer including mine as partial. Redaction that preserves document utility still leaks. The thing to avoid is a defense that reports a number without an adaptive attacker on the other side of it, because that number will be wrong in the direction you want it to be.
Code, benchmark, and reproduction steps: https://github.com/crossguard-ai/crossguard-py
Top comments (0)