47 lines
2.0 KiB
Python
47 lines
2.0 KiB
Python
"""Serving user-uploaded files without handing the browser a script.
|
|
|
|
Uploads reach these routes from people, and one of the accepted image types is
|
|
not inert: an SVG is an XML document that may carry <script>, and it is wanted
|
|
for floor-plan maps and branding, where vector is the right format. Loaded
|
|
through an <img> tag a script inside it never runs, so the tiles and maps in
|
|
this app were never the risk. Opening the file's own URL is - and the
|
|
application image route is public, so the URL is reachable without a session.
|
|
|
|
Rather than banning the format that has a real use, every uploaded file is
|
|
served with headers that make the document case inert:
|
|
|
|
Content-Security-Policy: default-src 'none'; sandbox
|
|
No script, no fetch, no plugin, and a unique opaque origin. This is what
|
|
stops an SVG (or an HTML file that talked its way past an allowlist)
|
|
executing in the site's origin and reading a token out of localStorage.
|
|
X-Content-Type-Options: nosniff
|
|
A file whose extension and bytes disagree is not re-typed by the browser
|
|
into something executable.
|
|
|
|
Both are cheap and apply to every upload route, so a new one added later starts
|
|
from the same place instead of repeating the reasoning.
|
|
"""
|
|
|
|
from flask import send_from_directory
|
|
|
|
# 'sandbox' with no allow-* tokens: opaque origin, scripts blocked, forms and
|
|
# popups blocked. default-src 'none' is belt and braces for a UA that ignores
|
|
# sandbox in a header.
|
|
UPLOAD_CSP = "default-src 'none'; sandbox"
|
|
|
|
|
|
def harden_upload_response(response):
|
|
"""Apply the inert-document headers to a response serving an upload."""
|
|
response.headers['Content-Security-Policy'] = UPLOAD_CSP
|
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
return response
|
|
|
|
|
|
def send_upload(directory, filename, **kwargs):
|
|
"""send_from_directory, with the upload headers applied.
|
|
|
|
Any keyword send_from_directory takes is passed through, so a route that
|
|
already sends as an attachment keeps doing so.
|
|
"""
|
|
return harden_upload_response(send_from_directory(directory, filename, **kwargs))
|