Serve an uploaded file as data, not as a document that can run
An SVG is an XML document that may carry a script, and it is an accepted image type because floor-plan maps and branding genuinely want vector. Loaded through an img tag that script never runs, so the tiles and maps were never the risk. Opening the file's own URL is - and the application image route is public, so that URL needs no session. Every route that serves an upload now goes through one helper that sends Content-Security-Policy: default-src 'none'; sandbox, and nosniff. Seven routes across core and five plugins, so a new one added later starts from the same place rather than repeating the reasoning. Banning the format instead would have cost the maps their only sensible one. The app also sent no security headers at all. It now sets nosniff, frame-ancestors self (as X-Frame-Options too, for the display bays' browsers) and a referrer policy. Deliberately NOT a page-wide CSP: this serves an SPA with inline styles, so a real script-src policy is a change worth making with the frontend in front of you, and a permissive header claiming one would be worse than having none. Contract 0.19.0. send_upload is on the shopdb.api surface, because a plugin serving user-supplied bytes should not have to remember these headers. The same bump records that get_dashboard_widgets has taken data and shape rather than a component name since the dashboard was rebuilt - that shipped without a bump, while BasePlugin and PLUGIN-HOOKS.md both still documented the shape nothing renders, which is how five plugins came to declare widgets pointing at components nobody had written.
This commit is contained in:
@@ -48,7 +48,16 @@ from .plugins import plugin_manager
|
||||
# the literal values of C:\Enrollment\display-type.txt - so a plugin holding its
|
||||
# own copy of that map (geenforce did) can read core's instead of drifting from
|
||||
# it. Additive names on the import surface, minor bump.
|
||||
__contract_version__ = '0.18.0'
|
||||
# 0.19.0: get_dashboard_widgets returns DATA AND SHAPE, not a component name.
|
||||
# The old shape ('name' + 'component' + 'size') named a Vue component per
|
||||
# widget, which cannot survive a lean build - a plugin's component may never be
|
||||
# staged into the frontend bundle (ADR-013) - and five plugins were declaring
|
||||
# widgets that pointed at components nobody had written. Core now owns the
|
||||
# renderers and a card declares id / title / endpoint / render / severity /
|
||||
# permission / empty / position. BREAKING for any plugin still using the old
|
||||
# shape, which is why it is recorded here: the change itself shipped earlier
|
||||
# without a bump, and a contract that changes silently is not a contract.
|
||||
__contract_version__ = '0.19.0'
|
||||
|
||||
# Product release version (see ADR-007). The product version and the
|
||||
# plugin-contract version above are distinct series with independent
|
||||
@@ -134,6 +143,41 @@ def create_app(config_name: str = None) -> Flask:
|
||||
identity = jwt_data["sub"]
|
||||
return db.session.get(User, int(identity))
|
||||
|
||||
@app.after_request
|
||||
def apply_security_headers(response):
|
||||
"""Baseline response headers. The app shipped with none of these.
|
||||
|
||||
Deliberately the three that cost nothing and break nothing:
|
||||
|
||||
nosniff a response whose bytes and declared type disagree is
|
||||
not re-typed by the browser into something executable.
|
||||
This is the general form of the upload problem that
|
||||
shopdb/utils/uploads.py addresses per-file.
|
||||
frame-ancestors
|
||||
same-origin only. The kiosks open routes directly
|
||||
rather than framing them, so this costs the fleet
|
||||
nothing and stops the UI being framed elsewhere and
|
||||
clicked through. Sent as X-Frame-Options too, because
|
||||
the display bays run browsers old enough to want it.
|
||||
Referrer-Policy
|
||||
an asset id or a hostname in a path is not handed to
|
||||
whatever a user clicks through to.
|
||||
|
||||
NOT a full page CSP. This app serves an SPA with inline styles, so a
|
||||
real script-src policy is a change worth making on its own with the
|
||||
frontend in front of you - claiming one here by adding a permissive
|
||||
header would be worse than having none.
|
||||
"""
|
||||
response.headers.setdefault('X-Content-Type-Options', 'nosniff')
|
||||
response.headers.setdefault('X-Frame-Options', 'SAMEORIGIN')
|
||||
response.headers.setdefault('Referrer-Policy',
|
||||
'strict-origin-when-cross-origin')
|
||||
# setdefault: an upload response has already declared its own, stricter
|
||||
# Content-Security-Policy, and this must not weaken it.
|
||||
response.headers.setdefault('Content-Security-Policy',
|
||||
"frame-ancestors 'self'")
|
||||
return response
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user