fix: log index failures, and tidy the CSP and the API error response
Build and Push Image / Unit tests (push) Successful in 1m6s
Build and Push Image / Build and push image (push) Skipped
Build and Push Image / Deploy to the cluster (push) Skipped

Round-2 review findings.

- `_ensure_indexes` swallowed a `PyMongoError` with a bare `pass`. An index
  failure does not stop the app serving, but it means the lookup is a
  collection scan, which gets slow as the data grows — so it is now logged
  as a warning with the traceback.

- The Content-Security-Policy string is now a module-level `CSP` constant
  rather than inline in the response handler, and it gained
  `object-src 'none'` (the app uses no plugins). The test now also asserts
  that neither `unsafe-inline` nor `unsafe-eval` appears — either would
  defeat the point of the policy.

- `api_run` returned a bare dict for its 404 while the success path used
  `jsonify`. It is now `jsonify({"error": "not found"})` for consistency.
This commit is contained in:
2026-11-09 12:36:17 +10:00
parent bf0f88dd70
commit 2a9e0ad15e
4 changed files with 34 additions and 9 deletions
+21 -6
View File
@@ -16,6 +16,26 @@ from flask_wtf import CSRFProtect
__all__ = ["create_app"] __all__ = ["create_app"]
#: The Content-Security-Policy the app sends with every response.
#:
#: The app serves only its own vendored assets — CSS, JavaScript, fonts and
#: the logo are all in ``static/`` — so a strict ``'self'`` policy is
#: enforceable. There is no ``'unsafe-inline'`` anywhere, which is why the
#: templates carry no inline scripts or style attributes.
CSP = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self'; "
"img-src 'self' data:; "
"font-src 'self'; "
"connect-src 'self'; "
"form-action 'self'; "
"object-src 'none'; "
"frame-ancestors 'none'; "
"base-uri 'self'"
)
class ImproperlyConfigured(RuntimeError): class ImproperlyConfigured(RuntimeError):
"""Raised when the app is missing configuration it cannot safely default. """Raised when the app is missing configuration it cannot safely default.
@@ -121,12 +141,7 @@ def create_app(config: dict | None = None) -> Flask:
response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("X-Frame-Options", "DENY") response.headers.setdefault("X-Frame-Options", "DENY")
response.headers.setdefault("Referrer-Policy", "same-origin") response.headers.setdefault("Referrer-Policy", "same-origin")
response.headers.setdefault( response.headers.setdefault("Content-Security-Policy", CSP)
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'; "
"img-src 'self' data:; font-src 'self'; connect-src 'self'; "
"form-action 'self'; frame-ancestors 'none'; base-uri 'self'",
)
return response return response
@app.errorhandler(404) @app.errorhandler(404)
+8 -2
View File
@@ -59,8 +59,14 @@ class BeerRepository:
[("beer_run_id", 1), ("date", 1)], name="run_date_1" [("beer_run_id", 1), ("date", 1)], name="run_date_1"
) )
except PyMongoError: except PyMongoError:
# An index failure must not stop the app from serving. # An index failure must not stop the app from serving, but it
pass # should be visible: without the index the lookup is a collection
# scan, which gets slow as the data grows.
logger.warning(
"Could not create the Mongo indexes; the lookups will be "
"collection scans until they exist",
exc_info=True,
)
# -- users -------------------------------------------------------------- # # -- users -------------------------------------------------------------- #
+1 -1
View File
@@ -196,7 +196,7 @@ def api_run(run_id: str):
"""JSON detail of a single run.""" """JSON detail of a single run."""
run = repo().run(run_id) run = repo().run(run_id)
if run is None: if run is None:
return {"error": "not found"}, 404 return jsonify({"error": "not found"}), 404
meta = repo().run_metadata(run_id) meta = repo().run_metadata(run_id)
summary = run_summary(run, og_override=meta.get("og_points")) summary = run_summary(run, og_override=meta.get("og_points"))
+4
View File
@@ -491,9 +491,13 @@ def test_security_headers_are_set(logged_in):
assert csp is not None assert csp is not None
assert "default-src 'self'" in csp assert "default-src 'self'" in csp
assert "script-src 'self'" in csp assert "script-src 'self'" in csp
assert "object-src 'none'" in csp
assert "frame-ancestors 'none'" in csp assert "frame-ancestors 'none'" in csp
# No CDN is used, so nothing external should be allowed. # No CDN is used, so nothing external should be allowed.
assert "https://" not in csp assert "https://" not in csp
# 'unsafe-inline' would defeat the point of the policy.
assert "unsafe-inline" not in csp
assert "unsafe-eval" not in csp
def test_security_headers_are_set_on_the_login_page(client): def test_security_headers_are_set_on_the_login_page(client):