- src/image-ingress.js: sniff magic bytes independent of declared MIME,
reject polyglots (embedded ZIP/script payloads), enforce 4 MB body cap
and 4096 px dimension cap before decode, re-encode via fixed-argv
Pillow subprocess with 15s timeout, strip EXIF/GPS/XMP/tEXt metadata,
fail closed with sanitized short errors and manual fallback.
- scripts/reencode_image.py: defensive decoder; decompression-bomb
guarded; prints one JSON verdict line, never image bytes.
- src/rate-limiter.js + server.mjs /api/analyze: bounded fixed-window
per-client limiter, 429 with retry-after, no payload retention.
- tests/image-ingress.test.js: hostile synthetic fixtures only
(spoofed MIME, GIF/ZIP polyglots, truncated/garbage/empty images,
12000x12000 bomb, 6000x6000 oversized, EXIF+GPS, SVG-with-script);
asserts provider is never reached on rejection and error messages
contain no bytes/base64/stacks.
Closes#16