initial: novel-app snapshot
This commit is contained in:
44
security.py
Normal file
44
security.py
Normal 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
|
||||
Reference in New Issue
Block a user