fix: A20 (partiel) — CSP nonce par requête, unsafe-inline sort de script-src (v7.7.0)
- ContentSecurityPolicyMiddleware : nonce aléatoire par requête dans la
ContextVar `CSP_NONCE` (posée avant `call_next` → visible des templates),
`script-src 'self' 'unsafe-eval' 'nonce-…'` — plus aucun script inline
sans nonce ne tourne (fin des XSS injectés en JS)
- 38 tags `<script>` des templates : `nonce="{{ csp_nonce() }}"` (passage
scripté, vérifié : 0 restant) ; `LOCAL_LOGIN_HTML` (constante de module) :
helper `_with_nonce()` au rendu ; collections.py : 3 scripts Python
(chart/form/map) noncés
- `<meta name="htmx-config" content='{"inlineScriptNonce": …}'>` dans base.html
: htmx ré-injecte les <script> des réponses boostées avec le bon nonce
- `script-src-attr 'unsafe-inline'` : les 74 handlers `onclick=` inline
restent couverts (le nonce les aurait désactivés aussi)
- chart.js (cdn.jsdelivr.net) et leaflet (unpkg) ajoutés à script-src/style-src
: vues chart/map déjà BLOQUÉES par la CSP depuis toujours
(commentaire ponytail: upgrade = vendoriser puis retirer les hôtes)
- reste d'A20 : unsafe-eval (Alpine x-data → @alpinejs/csp), externalisation
JS (A27), resserrer img-src/connect-src
test : test_csp_nonce_per_request (page base.html + page hors template,
nonce unique par requête)
suite **1037/1037** · `ruff check app tests` OK · docs à jour
This commit is contained in:
+13
-3
@@ -9,6 +9,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from app.auth.session import SessionManager
|
||||
from app.config import settings
|
||||
from app.templating import CSP_NONCE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["auth"], prefix="/auth")
|
||||
@@ -31,6 +32,15 @@ def get_redirect_uri(request: Request) -> str:
|
||||
host = fwd_host.split(",")[0].strip() or request.headers.get("host", "localhost:8080")
|
||||
return f"{scheme}://{host}/auth/callback"
|
||||
|
||||
def _with_nonce(html: str) -> str:
|
||||
"""A20 : injecte le nonce CSP au moment du rendu.
|
||||
|
||||
`LOCAL_LOGIN_HTML` est une constante de module — le nonce, lui, est par
|
||||
requête, donc il ne peut être figé qu'ici.
|
||||
"""
|
||||
return html.replace("<script>", f'<script nonce="{CSP_NONCE.get()}">', 1)
|
||||
|
||||
|
||||
LOCAL_LOGIN_HTML = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -148,7 +158,7 @@ async function handleLogin(e){e.preventDefault();const email=document.getElement
|
||||
@router.get("/register")
|
||||
async def register_page(request: Request):
|
||||
"""Show the registration page (local login page with register tab active)."""
|
||||
return HTMLResponse(LOCAL_LOGIN_HTML.replace(
|
||||
return HTMLResponse(_with_nonce(LOCAL_LOGIN_HTML.replace(
|
||||
'class="tab active" onclick="switchTab(\'login\')"',
|
||||
'class="tab" onclick="switchTab(\'login\')"'
|
||||
).replace(
|
||||
@@ -163,7 +173,7 @@ async def register_page(request: Request):
|
||||
).replace(
|
||||
'id="submit-btn">Login<',
|
||||
'id="submit-btn">Register<'
|
||||
), status_code=200)
|
||||
)), status_code=200)
|
||||
|
||||
|
||||
@router.get("/login")
|
||||
@@ -172,7 +182,7 @@ async def login(request: Request, provider: str = Query("gitea")):
|
||||
# Local login page (POST handled by /auth/local-login)
|
||||
from fastapi.responses import HTMLResponse
|
||||
if provider == "local":
|
||||
return HTMLResponse(LOCAL_LOGIN_HTML, status_code=200)
|
||||
return HTMLResponse(_with_nonce(LOCAL_LOGIN_HTML), status_code=200)
|
||||
|
||||
# OAuth flow — check if provider is configured
|
||||
from app.auth.providers import get_provider
|
||||
|
||||
Reference in New Issue
Block a user