initial: novel-app snapshot

This commit is contained in:
omo
2026-08-17 17:11:30 +08:00
commit 1ae06209ac
34 changed files with 2821 additions and 0 deletions

44
security.py Normal file
View File

@@ -0,0 +1,44 @@
"""CSRF protection (session-scoped token, no external dependency)."""
import hmac
import logging
import secrets
from flask import abort, request, session
logger = logging.getLogger(__name__)
def get_csrf_token():
"""Return the per-session CSRF token, generating one on first use."""
token = session.get("_csrf_token")
if not token:
token = secrets.token_hex(32)
session["_csrf_token"] = token
return token
def validate_csrf():
"""Abort 403 if the CSRF token doesn't match the session token.
Token may arrive in the form field "csrf_token" (POST), the JSON body
field "csrf_token" (fetch JSON POST), or as the query parameter
csrf_token (GET SSE endpoints).
"""
expected = session.get("_csrf_token")
provided = request.form.get("csrf_token", "") or request.args.get("csrf_token", "")
if not provided and request.is_json:
body = request.get_json(silent=True) or {}
provided = body.get("csrf_token", "")
if not expected or not provided or not hmac.compare_digest(expected, provided):
logger.warning(
"CSRF validation failed for %s from %s",
request.path,
request.remote_addr,
)
abort(403)
def init_app(app):
"""Expose csrf_token() to Jinja templates as a global."""
app.jinja_env.globals["csrf_token"] = get_csrf_token