From 2fb357ae7740820a00b3e753d6462393bed018ef Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Wed, 21 Oct 2026 18:17:19 +0000 Subject: [PATCH] Set security headers on all responses The app set no security headers at all, and the reverse proxy sets none either. - Content-Security-Policy: default-src 'none' with 'self' only for styles, images and fonts (MAX has no JavaScript), form-action 'self' (also blocks the http:// form action downgrade), frame-ancestors 'none' and base-uri 'none'. - X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: same-origin. - Strict-Transport-Security with max-age of 7 days, not in debug mode. The headers are set in after_request, so they also apply to static files, 404 and error responses. Co-Authored-By: Claude Opus 5.5 --- max/app.py | 2 ++ max/security_headers.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 max/security_headers.py diff --git a/max/app.py b/max/app.py index f59464e..bebbd2f 100644 --- a/max/app.py +++ b/max/app.py @@ -16,6 +16,7 @@ from .middlewares import ( add_middleware_stack, ) from .routes import init_routes +from .security_headers import init_security_headers from .sessions import SessionInterface from .translations import TranslationHandler @@ -64,6 +65,7 @@ def main(): register_cli(app) init_auth(app) init_routes(app) + init_security_headers(app) return app diff --git a/max/security_headers.py b/max/security_headers.py new file mode 100644 index 0000000..8f76f8f --- /dev/null +++ b/max/security_headers.py @@ -0,0 +1,29 @@ +CONTENT_SECURITY_POLICY = "; ".join( + ( + "default-src 'none'", + "style-src 'self'", + "img-src 'self'", + "font-src 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "base-uri 'none'", + ) +) +HSTS_MAX_AGE = 7 * 24 * 60 * 60 # in seconds + + +def init_security_headers(app): + headers = { + "Content-Security-Policy": CONTENT_SECURITY_POLICY, + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "same-origin", + } + if not app.debug: + headers["Strict-Transport-Security"] = f"max-age={HSTS_MAX_AGE}" + + # after_request also covers static files, 404 and error responses + @app.after_request + def set_security_headers(response): + response.headers.update(headers) + return response