Source code for shopyo.api.security
from urllib.parse import urljoin
from urllib.parse import urlparse
import secrets
from flask import request, session, g
"""
Security utilities for Shopyo.
Includes:
- Safe URL redirection (`is_safe_redirect_url`, `get_safe_redirect`)
- CSRF token generation and validation (`generate_csrf_token`, `validate_csrf_token`)
- Jinja2 context processor helper (`inject_csrf_token`)
CSRF protection for form-based routes is handled globally by Flask-WTF's
``CSRFProtect`` (initialised in ``shopyo/init.py``). API clients can use
``generate_csrf_token`` and ``validate_csrf_token`` directly, or send the
token via the ``X-CSRFToken`` header or ``csrf_token`` form field.
The legacy ``csrf_protect`` decorator has been removed — it was redundant
with Flask-WTF's global protection.
"""
# from https://security.openstack.org/guidelines/dg_avoid-unvalidated-redirects.html
[docs]
def is_safe_redirect_url(target):
"""
Corresponds to Djangos is_safe_url
Args:
target (String): url
Returns
-------
bool
"""
host_url = urlparse(request.host_url)
redirect_url = urlparse(urljoin(request.host_url, target))
return (
redirect_url.scheme in ("http", "https")
and host_url.netloc == redirect_url.netloc
)
[docs]
def get_safe_redirect(url):
"""
Returns url for root path if url not safe
Args:
url (String): url
Returns
-------
url or root page
"""
if url and is_safe_redirect_url(url):
return url
url = request.referrer
if url and is_safe_redirect_url(url):
return url
return "/"
CSRF_TOKEN_SESSION_KEY = "_csrf_token"
CSRF_TOKEN_HEADER = "X-CSRFToken"
CSRF_TOKEN_FORM_KEY = "csrf_token"
[docs]
def generate_csrf_token():
"""Generate a secure CSRF token.
Generates a secure CSRF token and stores it in the session if not present.
Returns
-------
str
The generated CSRF token.
"""
token = session.get(CSRF_TOKEN_SESSION_KEY)
if not token:
token = secrets.token_urlsafe(64)
session[CSRF_TOKEN_SESSION_KEY] = token
return token
[docs]
def validate_csrf_token(token):
"""Validate the CSRF token using constant-time comparison.
Parameters
----------
token : str
The CSRF token to validate.
Returns
-------
bool
True if the token is valid, False otherwise.
"""
session_token = session.get(CSRF_TOKEN_SESSION_KEY)
if not session_token or not token:
return False
# Constant-time comparison to prevent timing attacks
return secrets.compare_digest(session_token, token)
[docs]
def inject_csrf_token():
"""Inject CSRF token into Jinja2 template context.
Returns
-------
dict
Dictionary containing the CSRF token for template context.
"""
return {CSRF_TOKEN_FORM_KEY: generate_csrf_token()}