Lion Cipher Lab
Harden a multimodal ingestion pipeline against image-borne instructions
- AI Security
- Advanced
- ~100 min
- Free · no account required
Why it matters
The moment your application lets a person attach a picture or a document, you have quietly added a set of instruction-carrying channels that almost nobody enumerates, and the ones people do think of are not the dangerous ones. Everybody has now heard of prompt injection -- untrusted text that reaches a model's prompt and is read as instruction rather than as data -- and most teams have written some kind of filter for it. Almost all of those filters look at the chat box. Meanwhile the upload path is quietly doing something far more generous: a metadata extractor dumps every PNG text chunk and…
What you'll learn
- enumerating the FULL carrier set of a file upload: 20 instruction-carrying channels on one upload -- filename, four PNG text chunks, rendered pixels, JPEG COM and Exif UserComment, four SVG strings including a nested XML entity, PDF /Info and a compressed content stream, a ZIP member name and archive comment, and HTML alt/title/comment
- writing the parsers yourself in pure Python stdlib: PNG chunks with zlib and CRC-32, a JPEG marker stream, a TIFF/Exif IFD walked with struct, SVG through xml.etree, a PDF you build and re-read, and zipfile -- because you cannot defend a channel you have never parsed
- building a REAL text-in-image round trip: a 5x7 bitmap font, a renderer, and a decoder that reads pixels back to a string with 0 errors at three scales -- and knowing exactly why that result is a floor on vision-model capability, not a ceiling
- measuring the naive pipeline honestly: 20/20 carriers reach the prompt, 0/20 carry an origin label, and 94.2% of the prompt's characters came from a file a stranger uploaded
- pricing every defense instead of listing it: strip-all-metadata costs 46 of 64 legitimate fields; a denylist catches 3/12 evasion variants with 3/64 benign false positives; refusing images that contain text rejects 42.9% of real uploads
- provenance tagging: carrying every untrusted span in a typed field with a channel and origin so it never occupies the instruction channel, and watching a forged closing delimiter escape text fencing 3/3 times but a JSON envelope 0/3
- capability gating as the control that actually holds -- and the measured trap inside it: a TOOL-scoped capability still runs 20/20 injected calls once the user grants the tool, while an ARGUMENT-bound capability reaches 0/20 with the text still fully in context
- five failure modes with numbers attached: sanitize-then-reparse (PNG allows text chunks AFTER the image data), the channel nobody enumerated, decode-order bypasses, a single instruction split across two channels, and metadata a resizer copies into the thumbnail
- reading a pipeline diagram for trust boundaries: which component turns bytes into strings, which component concatenates, and which component is allowed to cause a side effect
- the professional posture -- treat every byte derived from user content as data with a provenance label, never as instruction; gate capabilities, not strings; and make the renderer and the tool broker the enforcement points instead of the prompt
Steps in this lab
- What this lab is, and the three words you need before you start
- Why the upload path and not the chat box (and what this is NOT)
- Zero-assumption setup: a terminal, Python, and a folder
- Create mmpipe.py -- the pipeline, the parsers, the font and the stub
- Create measure.py -- the harness that counts everything
- Run it once, and read the headline
- Section [1]: twenty channels on ONE upload
- Section [2]: the pixel channel, proved rather than asserted
- Section [3a]: strip all metadata -- what it closes, and what it costs
- Section [3b]: the keyword denylist, and the five variants that beat it
- Section [3c]: refuse images containing text -- the defense that deletes the product
- Section [3d]: provenance tagging -- what it proves, and what it does not
- Section [3e]: capability gating -- the one that holds, and the trap inside it
- Failure mode 4a: sanitize-then-reparse (the chunk that lives after the image)
- Failure mode 4b: the channel nobody enumerated
- Failure mode 4c: encoding order -- you scanned the wrong bytes
- Failure mode 4d: one instruction, two channels
- Failure mode 4e: the resizer that helpfully copies it forward
- Judgment: why this, not that -- input filtering versus action gating
- Find it in your own pipeline: the review a professional actually runs