The moment your platform accepts uploads, your CDN is serving files an adversary authored — on infrastructure carrying your name, your cookies and your users’ trust. UGC delivery is therefore a security pipeline with a CDN at the end: every safe platform runs the same four stages — isolated ingest, normalization, isolated serving, and a takedown path that verifiably works — and every UGC incident traces to a skipped stage.
The threat model: hostile authors, your domain
Name what an uploaded file can be: an HTML page wearing a .jpg extension, waiting for a browser to sniff it into executing scripts on your origin; a polyglot valid as two formats at once; a decompression bomb aimed at your thumbnailer; malware using your trusted domain as its distribution host; or simply illegal content, making takedown speed a legal property of your delivery. Two consequences shape everything. First, serving user bytes from your application domain is the root vulnerability — scripts executing there inherit your users’ sessions — so isolation by hostname is structural, not optional. Second, you cannot validate your way to safety by inspecting uploads; the reliable move is to never serve the uploaded bytes at all, which is section three’s job.
Ingest: signed uploads, quarantined storage
Uploads should go directly to quarantine storage via short-lived signed upload URLs — your application authorizes (per user, per size, per declared type), storage receives, and the upload stream never transits your app servers; the signing pattern is the upload-direction twin of signed URLs. Enforce limits at the door: size caps in the signed policy, count and rate limits per account (upload abuse is an abuse vector of its own, and the rate-limiting budgets apply). Everything lands in a quarantine bucket that no serving hostname can reach — scanned, but not trusted because it was scanned. The invariant to build and then audit: there exists no URL, signed or otherwise, that serves a byte from quarantine to a browser.
Normalize: transcode everything, trust nothing
The safety transform is re-encoding: decode every upload and re-encode it into your canonical formats — images to your chosen sizes and formats, video through your transcode ladder, audio likewise — so what reaches the serving bucket are bytes your pipeline authored. Polyglots, embedded scripts, malformed containers and steganographic payloads do not survive an honest decode-encode cycle, which is a stronger guarantee than any scanner’s signature list. Run the transcoders themselves as disposable sandboxes with strict resource ceilings, because decoders are historically where crafted-file exploits land, and a decompression bomb should kill one container, not one pipeline. Strip metadata deliberately — location EXIF is a user-privacy leak your platform should not republish — and write outputs to the serving bucket under content-addressed or otherwise unguessable paths, with the original retained in quarantine only as long as moderation and law require.
Serve: the separate domain and its headers
Serve UGC from a dedicated, cookieless hostname — ideally a separate registrable domain (the usercontent pattern), never a subdomain sharing cookies with your app — fronted by the CDN with caching as generous as any static estate, since normalized objects are immutable. Belt-and-braces the responses at the edge: exact Content-Type from your pipeline’s knowledge (never derived from the file), X-Content-Type-Options: nosniff, a restrictive Content-Security-Policy (a sandboxing policy on anything that could render as a document), and Content-Disposition: attachment for anything users download rather than view — the header machinery from security headers at the edge, tightened for hostile content. Private UGC (DMs, restricted posts) adds token authentication on this hostname; public UGC should stay tokenless and fully cacheable. The edge is also your enforcement point for hotlink policy and per-object kill switches, which brings us to takedowns.
Moderation, takedowns and purge reality
Moderation architecture is a publish-gate decision: hold-then-publish (nothing serves until checks pass) for higher-risk surfaces, publish-then-scan for velocity, most platforms landing on a risk-tiered mix — but whichever you choose, the delivery requirement is identical: removal must actually remove. A takedown is a database flag plus a purge, and the purge must be verified — request the object through the CDN after purging and confirm the 404/410, because cached copies at the edge are precisely the copies that matter legally; the mechanics and their per-platform propagation quirks are in purging without pain. Design object paths so one identity’s content can be purged as a group (per-user prefixes or tags), keep the moderation-to-purge pipeline automated and measured in seconds-to-minutes, and log takedown timestamps end to end — the interval between report and unreachable-at-edge is the number regulators and rights-holders will ask about. UGC delivery done right is boring: hostile bytes never serve, normalized bytes cache forever, and removal is one verified operation.
