Lion Cipher Lab
Serve user-uploaded files safely: a separate origin, Content-Disposition, nosniff and a sandbox CSP
- AppSec / DevSecOps
- Intermediate
- ~60 min
- Free · no account required
Why it matters
Accepting an upload safely is only half the job. The other half is what happens when someone OPENS that file later. If your app serves a user's file from its own origin and lets the browser treat it as a web page, a file someone uploaded can run script with your app's privileges: read the page, act as the logged-in user, and call your API. That is stored cross-site scripting delivered as an attachment, and it does not need a bug in your HTML templates at all. An .html file, an .svg with a script tag, or a file with no type at all whose bytes look like HTML is enough. Big platforms answered…
What you'll learn
- explaining the difference between an ORIGIN (scheme, host and port) and a SITE (registrable domain, port ignored), and why two ports on one host are two origins but one site
- building a hardened file-serving origin in standard-library Python that picks Content-Type from the file's magic bytes through a server-side allowlist instead of trusting the uploaded filename
- setting and justifying X-Content-Type-Options: nosniff, Content-Disposition: attachment (inline only for a small image allowlist), Content-Security-Policy: sandbox; default-src 'none', and Cross-Origin-Resource-Policy on every stored file
- writing a Content-Disposition download name the RFC 6266 way: an ASCII filename fallback plus an RFC 8187 filename* parameter for Unicode names
- auditing response headers with a urllib script and turning documented browser behavior (WHATWG MIME Sniffing, MDN) into a per-file verdict: download, renders inert, or script runs in which origin
- measuring defense in depth by removing one layer at a time, and explaining why nosniff alone or a separate origin alone is not enough
- proving that cookies do not isolate by port, and therefore why real deployments serve user content from a separate registrable domain with no shared Domain= cookie
- confirming header behavior in a real browser with DevTools, and naming what this design does NOT do (malware scanning, SVG opened as a document, capability URLs leaking)
Steps in this lab
- The problem, and the words you need
- Why serious platforms move user files to another domain
- Set up: check Python and make a folder
- Build the two serving styles in one server
- What each hardened header tells the browser
- Build the header audit (the browser rules, written as code)
- Run it and read the headers
- Measure it: which files would run script in your app's origin
- Take the layers away one at a time
- The cookie check: a separate origin is not a separate site
- Confirm it in a real browser with DevTools
- What this design does NOT do, and how a pro goes further