timmy-talking-turd/scripts/gen_ingress_fixtures.py
Timmy 517c8dbac3
Some checks failed
Quality gates / quality (pull_request) Failing after 2m39s
Harden image ingress: pinned runtime, header-bomb rejection, concurrency ceiling, bounded rate limiter, canonical data-URL/polyglot contract, 503-on-unavailable, body-read timeout
Closes the PR 63 hostile-review blockers:
1. Immutable pinned Python/Pillow runtime (TIMMY_PYTHON absolute, --verify-pin),
   deployment/runtime re-encode smoke gate in build_release + deploy_staging.
2. Header-only width/height/total-pixel/bomb rejection before full decode; proves
   6000x6000 and 12000x12000 stay resource bounded (RSS + address-space caps).
3. Fail-fast decoder concurrency ceiling; tests count actual spawned children.
4. Rate-limiter key cardinality hard-bounded under 20k+ unexpired identities,
   trusted-loopback-proxy identity, no spoofable forwarded headers, fixed-window
   boundary burst smoothed by two-window sliding count.
5. build_release explicitly syntax/gates every new JS module + Python re-encoder +
   production-runtime smoke; CI runs reencode-image test and the runtime pin smoke.
6. Robust polyglot contract via canonical container parsing; rejects uppercase/mixed
   script, appended HTML, ZIP local/EOCD and archive tails, data after canonical
   JPEG/PNG/WebP end; no naive compressed-byte scans (false-positive controls pass).
7. Canonical base64 data-URL grammar with byte-exact round trip; rejects missing/excess
   padding, whitespace/CRLF, malformed and noncanonical encodings; exact MIME policy.
8. Missing Pillow / runtime-unavailable maps to sanitized 503 + manual fallback.
9. Inbound body-read timeout and stop-on-oversize; preserves sanitized 413, base path,
   provider suppression, and temp cleanup; socket torn down on rejection.

Audited prior partial edits: reused the sound source modules, re-wired new tests into
the unit/syntax gates, fixed a non-canonical readJson that destroyed the socket before
delivering 413/408, and hardened test flakiness (port reuse, EPIPE, startup races).
2026-08-22 23:22:42 +00:00

131 lines
5.6 KiB
Python

#!/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"<!DOCTYPE html><html><body>not an image</body></html>")
# 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"<script>alert(1)</script>" * 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'<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)">'
b"<rect width='10' height='10'/></svg>")
# --- 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"<SCRIPT>alert(1)</SCRIPT>",
"ingress-tail-mixed-script.jpg": b"<ScRiPt>alert(1)</ScRiPt>",
"ingress-tail-html.jpg": b"<html><body><img src=x onerror=alert(1)></body></html>",
"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"<SCRIPT>alert(1)</SCRIPT>")
# 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"<script", b"<SCRIPT"):
if probe in entropy:
print("entropy control already contains", probe)
# Deterministic worst case: a valid JPEG carrying the exact archive/script byte
# sequences inside a COM (comment) segment, which is a legal part of the JPEG
# structure. Canonical parsing must accept it; substring scanning must not.
buried = bytearray(jpg_bytes)
comment_payload = b"PK\x03\x04PK\x05\x06<script>alert(1)</script>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")