|
24 | 24 |
|
25 | 25 | import requests |
26 | 26 | import yaml |
27 | | -from flask import Flask, Response, jsonify, render_template, request, session |
| 27 | +import secrets as _secrets_mod |
| 28 | + |
| 29 | +from flask import Flask, Response, g, jsonify, render_template, request, session |
28 | 30 |
|
29 | 31 | # Add services/ to path so we can import common.audit_chain |
30 | 32 | _services_root = str(Path(__file__).resolve().parent.parent.parent) |
@@ -146,20 +148,27 @@ def csrf_protect(): |
146 | 148 | return None |
147 | 149 |
|
148 | 150 |
|
| 151 | +@app.before_request |
| 152 | +def generate_csp_nonce(): |
| 153 | + """Generate a per-request CSP nonce for inline scripts and styles.""" |
| 154 | + g.csp_nonce = _secrets_mod.token_urlsafe(24) |
| 155 | + |
| 156 | + |
149 | 157 | @app.context_processor |
150 | | -def inject_csrf_token(): |
151 | | - """Expose CSRF token to all templates.""" |
| 158 | +def inject_template_globals(): |
| 159 | + """Expose CSRF token and CSP nonce to all templates.""" |
152 | 160 | token = session.get("csrf_token", "") |
153 | | - return {"csrf_token": token} |
| 161 | + return {"csrf_token": token, "csp_nonce": getattr(g, "csp_nonce", "")} |
154 | 162 |
|
155 | 163 |
|
156 | 164 | @app.after_request |
157 | 165 | def add_security_headers(response): |
158 | 166 | """Add defense-in-depth HTTP security headers to every response.""" |
| 167 | + nonce = getattr(g, "csp_nonce", "") |
159 | 168 | response.headers["Content-Security-Policy"] = ( |
160 | 169 | "default-src 'self'; " |
161 | | - "script-src 'self' 'unsafe-inline'; " |
162 | | - "style-src 'self' 'unsafe-inline'; " |
| 170 | + f"script-src 'self' 'nonce-{nonce}'; " |
| 171 | + f"style-src 'self' 'nonce-{nonce}' 'unsafe-inline'; " |
163 | 172 | "img-src 'self' data:; " |
164 | 173 | "media-src 'self' data:; " |
165 | 174 | "font-src 'self'; " |
|
0 commit comments