#!/usr/bin/env python3 """Generate synthetic hostile fixtures for image-ingress security tests. All content is synthetic test data. No real medical images, no external uploads. """ import os from PIL import Image, PngImagePlugin D = os.path.join(os.path.dirname(__file__), "..", "tests", "fixtures") D = os.path.abspath(D) os.makedirs(D, exist_ok=True) img = Image.new("RGB", (64, 64)) for y in range(64): for x in range(64): img.putpixel((x, y), (x * 4 % 256, y * 4 % 256, 128)) # 1. Clean small JPEG (valid control) img.save(os.path.join(D, "ingress-clean.jpg"), "JPEG", quality=90) # 2. JPEG with EXIF metadata (Make/Model + GPS IFD pointer) ex = img.getexif() ex[0x010F] = "HostileCam" ex[0x0110] = "Model-X" gps_ifd = ex.get_ifd(0x8825) gps_ifd[1] = "N" # GPSLatitudeRef gps_ifd[2] = (44, 30, 0) # GPSLatitude (degrees, minutes, seconds) gps_ifd[4] = (68, 15, 0) # GPSLongitude img.save(os.path.join(D, "ingress-exif.jpg"), "JPEG", quality=90, exif=ex) # 3. PNG with tEXt metadata chunks png_meta = PngImagePlugin.PngInfo() png_meta.add_text("Comment", "sensitive-metadata") png_meta.add_text("GPS", "lat:44.0 lon:-68.0") img.save(os.path.join(D, "ingress-metadata.png"), "PNG", pnginfo=png_meta) # 4. Spoofed content: HTML masquerading as an image with open(os.path.join(D, "ingress-spoofed.html"), "wb") as f: f.write(b"not an image") # 5. GIF-header polyglot with embedded script payload poly = (b"GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00" b"\x00\x02\x00;" + b"" * 4) with open(os.path.join(D, "ingress-polyglot.gif"), "wb") as f: f.write(poly) # 6. ZIP-in-JPEG polyglot (GIFAR-style) jpg_bytes = open(os.path.join(D, "ingress-clean.jpg"), "rb").read() zip_poly = jpg_bytes[:2] + b"PK\x03\x04" + jpg_bytes[2:10] + b"PK\x05\x06" + b"\x00" * 18 with open(os.path.join(D, "ingress-zip-polyglot.jpg"), "wb") as f: f.write(zip_poly) # 7. Truncated JPEG (SOI present, cut before EOI) with open(os.path.join(D, "ingress-truncated.jpg"), "wb") as f: f.write(jpg_bytes[: len(jpg_bytes) // 2]) # 8. Decompression bomb: 12000x12000 sparse PNG, tiny on disk bomb = Image.new("L", (12000, 12000), 7) bomb.save(os.path.join(D, "ingress-bomb.png"), "PNG", optimize=True) print("bomb size:", os.path.getsize(os.path.join(D, "ingress-bomb.png"))) # 9. Oversized-dimension JPEG (6000x6000, small on disk) big = Image.new("RGB", (6000, 6000), (90, 90, 90)) big.save(os.path.join(D, "ingress-oversized.jpg"), "JPEG", quality=40) print("oversized size:", os.path.getsize(os.path.join(D, "ingress-oversized.jpg"))) # 10. Random garbage with jpeg extension with open(os.path.join(D, "ingress-garbage.jpg"), "wb") as f: f.write(os.urandom(2048)) # 11. Empty file open(os.path.join(D, "ingress-empty.jpg"), "wb").close() # 12. SVG with embedded script with open(os.path.join(D, "ingress-script.svg"), "wb") as f: f.write(b'' b"") # --- Trailing-data / container polyglots that bypass naive substring scans --- # Every one of these is a structurally valid image followed by appended bytes. # A canonical parser must reject them on the trailing data itself, not on a # signature keyword, so casing and container choice cannot evade the check. TRAILERS = { "ingress-tail-upper-script.jpg": b"", "ingress-tail-mixed-script.jpg": b"", "ingress-tail-html.jpg": b"", "ingress-tail-zip-eocd.jpg": b"PK\x05\x06" + b"\x00" * 18, "ingress-tail-zip-local.jpg": b"PK\x03\x04" + b"\x00" * 26, "ingress-tail-rar.jpg": b"Rar!\x1a\x07\x00", "ingress-tail-7z.jpg": b"7z\xbc\xaf\x27\x1c", "ingress-tail-gzip.jpg": b"\x1f\x8b\x08\x00" + b"\x00" * 6, "ingress-tail-single-nul.jpg": b"\x00", } for name, trailer in TRAILERS.items(): with open(os.path.join(D, name), "wb") as f: f.write(jpg_bytes + trailer) # Trailing data appended to a valid PNG (chunk stream ends at IEND). png_bytes = open(os.path.join(D, "ingress-metadata.png"), "rb").read() with open(os.path.join(D, "ingress-tail-after-iend.png"), "wb") as f: f.write(png_bytes + b"") # 13. False-positive control: a legitimate photo-like JPEG whose *compressed* # entropy bytes contain archive/script byte sequences by construction. A # canonical parser must ACCEPT this; a naive substring scanner rejects it. import random random.seed(1337) noise = Image.new("RGB", (160, 160)) for y in range(160): for x in range(160): noise.putpixel((x, y), (random.randrange(256), random.randrange(256), random.randrange(256))) noise.save(os.path.join(D, "ingress-entropy-control.jpg"), "JPEG", quality=95) entropy = open(os.path.join(D, "ingress-entropy-control.jpg"), "rb").read() for probe in (b"PK\x03\x04", b"PK\x05\x06", b"alert(1)Rar!\x1a\x07\x00" com_segment = b"\xff\xfe" + (len(comment_payload) + 2).to_bytes(2, "big") + comment_payload buried[2:2] = com_segment # insert immediately after SOI with open(os.path.join(D, "ingress-buried-signatures.jpg"), "wb") as f: f.write(bytes(buried)) print("fixtures written")