Skip to content

Static file serving

GET requests are answered directly by nginx via sendfile – Python is never on the read path. This page covers the security headers you almost certainly want for that path, and when it's safe to drop them.

Stored-XSS hardening

PutFS accepts arbitrary blobs from authorised PUT-ers and serves them back from the same origin. Without hardening, nginx's default mime.types map sets Content-Type from the file extension – .htmltext/html, .svgimage/svg+xml, .jsapplication/javascript. Any client who can PUT a file can then upload a .html containing <script> and trick a browser into executing it on the storage origin.

The shipped docker-compose.yml and contrib/putfs.nginx.conf defend against this with four directives in location /:

types { } default_type application/octet-stream;
add_header Content-Disposition 'attachment' always;
add_header X-Content-Type-Options 'nosniff' always;
add_header Content-Security-Policy "default-src 'none'; sandbox" always;
Directive Effect
types { } default_type application/octet-stream; Empties the type-by-extension map – every served file is application/octet-stream. Browsers won't render or execute it.
Content-Disposition: attachment Force-download, no inline rendering. The browser saves the file rather than evaluating it.
X-Content-Type-Options: nosniff Stops the browser from second-guessing the Content-Type and inferring HTML/JS from content.
Content-Security-Policy: default-src 'none'; sandbox Belt-and-suspenders for browsers that ignore the others; disables script execution and origin access on the served document.

Don't disable this without thinking

The four directives above are the difference between "blob storage" and "anyone with PUT can host an XSS payload on your storage origin". If you need PutFS to serve typed content (e.g. host a static site), do it from a separate hostname that nobody is ever logged into for anything else – not the same origin as anything cookie-bearing. Cookies, OAuth tokens, and localStorage are all same-origin-scoped; a stored XSS on the storage origin steals everything that origin sees.

If your trust model genuinely allows uploaders to host typed content (single-tenant, all uploaders are admins), remove the four directives in location /. Pair the change with a Content-Security-Policy you actually want, and ideally still keep X-Content-Type-Options: nosniff.

Further reading