Serving Uploaded Media Safely

Repo-based media is committed to your repository and served from your site's public folder, so every uploaded file comes from your site's own domain. If a browser runs one of those files as a page or a script, it runs with your site's cookies and storage.

As of @tinacms/cli 4.0.1, TinaCMS's upload route rejects HTML, XML and script uploads. Not every upload path runs that check. SVG stays allowed, because logos and icons rely on it, and an SVG can still run script when someone opens its URL directly. The settings below close that gap and protect any file that reaches your repository another way.

1. Stop browsers guessing the type

Send X-Content-Type-Options: nosniff for your media path. The browser then trusts the content type your server declares instead of guessing one from the file's bytes.

2. Stop an uploaded file running as a page

Send a Content Security Policy on the media path that allows no script and sandboxes the file:

This applies only when someone opens a file directly. Images and SVGs placed with <img> on your own pages are unaffected, because a CSP header governs a document, not an image loaded into one.

default-src 'none' can stop PDFs opening inline in some browsers, because it also blocks the built-in viewer. Check the browsers you support, and don't send this Content Security Policy for .pdf if they need it.

3. Serve media from its own domain where you can

A separate domain shares no cookies or storage with your site, so nothing uploaded there can act as your site. External stores (S3, Cloudinary, DigitalOcean Spaces, Azure) give you this when you serve from the provider's own hostname or a dedicated media subdomain. Avoid putting a bucket behind a path on your main domain. See External Media Providers for setup.

Examples

Replace /uploads with your mediaRoot. Each example sets X-Content-Type-Options and the Content Security Policy on the media path.

Next.js (next.config.js):

Netlify (_headers in your publish directory, e.g. public/_headers):

Vercel (vercel.json):

nginx (an add_header in a location replaces the ones inherited from server, so repeat headers like Strict-Transport-Security here):

Who can upload

Anyone with upload access can put a file on your domain. Give media access only to editors you trust, which matters most on self-hosted setups with several editors.

Last Edited: October 10, 2026