Building Apps with AILesson 75 of 87

Contents

Building Apps with AI

Lesson 75 of 87. Sign in to keep your place.

Module 8 · The security holes AI code has by default

Uploads that run

8 min read

The file is not what it says

An upload arrives with a name, an extension, and a declared content type, and the uploader chose all three. invoice.pdf can contain a Windows executable; photo.jpg can be an HTML page; logo.svg can be a document with script in it. The uploads lesson in the data block set the size and type limits. This lesson is about what a limit has to check, and what happens to the file once it is stored.

Read the bytes

File formats begin with a signature. A JPEG starts with FF D8 FF, a PNG with 89 50 4E 47, a PDF with %PDF-. A library such as file-type reads the first few bytes and reports what the file actually is, regardless of its name:

TypeScript
import { fileTypeFromBuffer } from "file-type";
const kind = await fileTypeFromBuffer(buffer);
if (!kind || !["image/jpeg", "image/png", "image/webp"].includes(kind.mime)) {
  return new Response("Unsupported file type", { status: 415 });
}

An allowlist of what you accept, decided by content. Everything else refused, including the types you did not think about. If the feature is avatars, the list is three image formats and nothing else; the day someone needs a PDF is the day the list grows by one, deliberately.

SVG is a document

SVG is the one image format that is also a program. It is XML, it can contain script and external references, and a browser opening an SVG file directly treats it as a page. Served from your domain, that page runs on your origin, which is the question above: an avatar that steals sessions.

Either refuse SVG for user uploads, the common choice, or sanitise it with a tool built for it, and serve it, like everything else uploaded, from somewhere that is not your app’s origin.

Serve from somewhere else

Uploaded files go to object storage, and object storage gives them an address on its own domain: pub-a1b2.r2.dev/..., or a bucket domain you configure. Script in a file served from that domain runs on that domain, where there is no session cookie and nothing to steal. This single arrangement neutralises most upload-borne XSS whatever the file contains, which is why the uploads lesson insisted on it before security came up.

Two headers complete it. Content-Type set by your code from the detected type, not from the upload. And Content-Disposition: attachment on anything that is not an image meant to display inline, so that the browser downloads rather than renders it.

The name on disk

The uploader’s filename is used for one thing: showing it back to them. The key in the bucket is generated by the server: a random id under the user’s own prefix, as the earlier lesson showed. A filename used as a path allows ../../.env and similar; a filename used as a key allows overwriting another user’s file by guessing its name. Neither is possible when the server names everything.

Processing is an attack surface

Every tool that opens an uploaded file is code parsing hostile input. Image libraries have a long record of vulnerabilities triggered by crafted files; ImageMagick’s history is the famous one. Three habits reduce the exposure. Keep the library current: npm audit and the dependency lesson apply. Process in a separate function or service, so that a compromise of the resizer is a compromise of a box with no database access. And set limits before parsing: a maximum pixel dimension, because a 1 KB PNG can declare itself to be 100,000 by 100,000 pixels and ask the resizer for 40 GB of memory.

Zip files are the same idea: a small archive that expands to terabytes. Cap the expanded size and the file count, or refuse archives.

The size limit that matters

The browser-side check on size is a courtesy. The server-side check has to happen before the whole body is read (through the signed-URL upload’s declared length, or the host’s body limit) because a route that reads a 2 GB body to discover it is too large has already spent the memory and the time.

Two minutes per feature

For every upload feature: send a file with a wrong extension and a right signature, and vice versa. Send an SVG. Send a file named with ../. Open the stored file’s URL directly and look at the domain in the address bar and the headers in the Network tab. If the domain is your app’s and the type came from the upload, the finding is written before the page has finished loading.

Every lesson here is open. Your progress is recorded once you pass the Building with AI exam.

Check your understanding

An avatar upload accepts image/svg+xml, checks the extension, stores the file, and serves it from /uploads/ on the app’s own domain. What can a user do with an SVG that contains script?

Pick the one you would defend. Your answer stays private.

Next lesson · 8 minThe admin page nobody protected

Stuck on something?Ask Beatriz, your AI tutor

How to cite: Addaly (2026) ‘Uploads that run’, in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/uploads-that-run (Accessed: 13 October 2026).