How to test MIME types and file type validation

Extensions, Content-Type headers and magic bytes: how to check that type validation cannot be fooled.

Published by SampleTestFiles. About 5 minutes to read.

A MIME type, formally a media type, is a label such as image/png or application/pdf. Browsers use it to decide how to display content, and servers use it to decide what to accept. The label is only useful if it is true, and testing type validation is mostly a matter of finding out whose word the server takes.

Three sources, three levels of trust

When a file is uploaded, its type can be read from three places.

  • The file name. The extension is whatever the user typed. It costs nothing to change.
  • The Content-Type of the upload part. The browser fills this in, usually from the extension. A script can send anything.
  • The content. Most binary formats begin with fixed signature bytes, often called magic bytes. Faking these takes real effort, and a file that passes a full parse is what it claims to be.

Sound validation uses the name and the header as hints for a helpful error message and makes the decision on content.

Signature bytes of common formats

FormatMIME typeLeading bytes (hex)As text
PDFapplication/pdf25 50 44 46 2D%PDF-
PNGimage/png89 50 4E 47 0D 0A 1A 0A.PNG....
JPEGimage/jpegFF D8 FF
GIFimage/gif47 49 46 38GIF8
WebPimage/webp52 49 46 46, then 57 45 42 50 at byte 8RIFF....WEBP
ZIP, DOCX, XLSX, PPTX, EPUBapplication/zip and others50 4B 03 04PK..
GZIPapplication/gzip1F 8B
MP4video/mp466 74 79 70 at byte 4ftyp
MP3 with ID3 tagaudio/mpeg49 44 33ID3
WAVaudio/wav52 49 46 46, then 57 41 56 45 at byte 8RIFF....WAVE

Every format page on this site lists the signature for that format, and you can look at the first bytes of any file yourself:

xxd -l 16 sample-png-10kb.png
file --mime-type -b sample-png-10kb.png

The first command prints the leading 16 bytes in hex. The second asks the file utility to identify the type from content, and prints image/png.

Formats that cannot be recognised by their first bytes

Signature checks have limits, and good tests aim at them.

  • ZIP-based formats. DOCX, XLSX, PPTX and EPUB all begin with the ZIP signature. Telling them apart means opening the archive and looking for [Content_Types].xml or the mimetype entry.
  • RIFF-based formats. WebP and WAV share their first four bytes. The distinguishing tag is at byte 8.
  • Text formats. CSV, JSON, plain text and most configuration files have no signature. The only real check is to parse them.
  • SVG and HTML. These are text, yet browsers treat them as active documents. Accepting them as "images" or "text" without sanitising is a common route to cross-site scripting.

Test cases

Use genuine sample files so that you know the correct answer in advance.

  1. Baseline. Upload one valid file of each accepted type. Confirm that the type your application stores matches the MIME type on the format page.
  2. Renamed file. Copy a PNG and give it a .pdf extension. Upload it to a PDF-only field. Expected: refused.
  3. Wrong header. Send a real PDF but declare it as image/png, using the command below. Expected: the server decides from content, so it either accepts the file as a PDF or refuses it. It must never store a PDF labelled as a PNG.
  4. No extension. Upload the file without an extension. Expected: handled by content, or refused with a clear message.
  5. Truncated file. Keep only the first 100 bytes of a PNG with head -c 100. The signature is intact but the image is not. A signature-only check accepts it; a decoder does not. Decide which behaviour you need.
  6. Office documents. Upload DOCX, XLSX and PPTX samples to a field that accepts them. Detection libraries frequently report application/zip, and an allow-list that omits it rejects valid documents.

The wrong-header case can be sent with curl, which lets you set the part's type explicitly:

curl -sS -o /dev/null -w '%{http_code}\n' \
  -F 'file=@sample-pdf-10kb.pdf;type=image/png' \
  https://your-app.example/upload

A minimal content check

This Node.js function reads the first bytes of a file and matches them against a few signatures. Real projects should use a maintained detection library, but the logic is the same.

import { open } from 'node:fs/promises';

const SIGNATURES = [
  { mime: 'application/pdf', bytes: [0x25, 0x50, 0x44, 0x46, 0x2d] },
  { mime: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
  { mime: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },
  { mime: 'image/gif', bytes: [0x47, 0x49, 0x46, 0x38] },
  { mime: 'application/zip', bytes: [0x50, 0x4b, 0x03, 0x04] },
];

export async function sniff(path) {
  const handle = await open(path);
  const { buffer } = await handle.read(Buffer.alloc(16), 0, 16, 0);
  await handle.close();
  const hit = SIGNATURES.find((s) => s.bytes.every((b, i) => buffer[i] === b));
  return hit ? hit.mime : 'application/octet-stream';
}

Serving files back

Validation on the way in is half of the job. When a stored file is downloaded or displayed:

  • Send the Content-Type you determined at upload, never one supplied by the uploader.
  • Send X-Content-Type-Options: nosniff, which tells browsers not to second-guess the declared type.
  • For anything that is not meant to be displayed inline, send Content-Disposition: attachment.
  • Serve user uploads from a separate domain where possible, so that a file that does execute has no access to your site's cookies.

To test this, upload an HTML sample, request its stored URL in a browser and confirm that it downloads instead of rendering.

Further reading

The rules browsers follow are defined in the WHATWG MIME Sniffing Standard, and registered types are listed in the IANA media types registry.

Files used in this guide

Sample files referred to in this guide
FileFormatSizeContentsDownload
10 KB PNG samplesample-png-10kb.png PNG 10 KB10,240 bytes 67 × 50 px Download PNG
10 KB PDF samplesample-pdf-10kb.pdf PDF 10 KB10,240 bytes Pages: 2 Download PDF
50 KB DOCX samplesample-docx-50kb.docx DOCX 50 KB51,200 bytes Headings, paragraphs, a bulleted list, a table and one image Download DOCX
10 KB WebP samplesample-webp-10kb.webp WebP 10 KB10,240 bytes 66 × 48 px Download WebP
File without an extensionsample-file-without-extension TXT 61 B61 bytes ASCII text Download TXT

Related guides