Security hardening, multi-site portability, and a documentation and API-surface sweep
This commit is contained in:
46
shopdb/utils/uploads.py
Normal file
46
shopdb/utils/uploads.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user