← Writing

Validate uploads by magic bytes, not file extension

A production 'PNG' turned out to be 137,138 bytes of a storage API's JSON response saved to disk — the file extension told us nothing, and the first 16 bytes would have told us everything.

A production PDF generator started failing with Unable to guess the image type on a clinic’s logo. The file was named clinicLogo.png, it lived where a PNG should live, and it was 137,138 bytes — a perfectly plausible size for a logo.

It was not a PNG. It was a storage API’s JSON directory-listing response, saved to disk with a .png name by an upstream bug. Every byte-level check would have caught it instantly: a real PNG opens with the fixed signature 89 50 4E 47; this file opened with {. But nothing in the pipeline ever looked at the bytes — the code trusted the extension, the logs said the PDF “was generated,” and the failure surfaced far downstream in an image decoder with a message that pointed nowhere near the cause.

The fix was a shared magic-byte validator: read the first bytes, match them against the known signatures (PNG, JPEG, GIF, WebP), and reject anything else at write time — when the file is saved — not at use time, when the context that would explain the corruption is long gone.

The part I’d emphasize to anyone shipping a validator like this: verify it against production data before trusting it. I ran the checks against the first 16 bytes of all 199 image objects in the production bucket. 198 passed. The single rejection was a genuine HEIC file — which was already breaking PDF generation today. Zero false positives, one true positive, measured rather than assumed.

Extensions are labels. Sizes are coincidences. The bytes are the file.

← All writing Book a call →
Book a call → WhatsApp