Files
max/max/middlewares.py
T
finnandClaude Opus 5.5 dc85c578a2 Protect against login CSRF without cookies for anonymous users
Anonymous users must not get cookies or session store entries, so a
CSRF token for /login is not possible. Instead, every unsafe request is
checked statelessly via Sec-Fetch-Site, falling back to Origin. Requests
without both headers are not from a browser and are accepted. The
expected origin comes from the new MAX_ORIGIN setting, which is required
outside of debug mode.

Further CSRF changes:
- Do not delete the CSRF token on a failed check. It added no security,
  but invalidated forms in open tabs and could be triggered cross-site.
- Treat HEAD and OPTIONS as safe methods.

Keep the login page free of sessions:
- Show login messages via ?msg=login_required|logout (fixed keys only)
  and the login error via the template context instead of flash().
- Only send the session deletion cookie if the client sent a cookie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:03:03 +00:00

102 lines
2.9 KiB
Python

import time
from flask import Response, request, session
from max.db import database
from max.exceptions import NotFound, PermissionDenied
from max.not_found_view import NotFoundView
from max.translations import TranslationHandler
class BaseMiddleware:
def __init__(self, app, handler):
self.app = app
self.handler = handler
for attr in ("no_auth", "no_csrf"):
if hasattr(handler, attr):
setattr(self, attr, True)
def __call__(self, *args, **kwargs):
raise NotImplementedError()
def get_real_remote_addr(app):
real_ip = request.headers.get("X-Real-IP", None)
if real_ip is None and app.debug:
real_ip = request.remote_addr
return real_ip
class LoggingMiddleware(BaseMiddleware):
def __call__(self, *args, **kwargs):
setattr(request, "real_remote_addr", get_real_remote_addr(self.app))
start = time.time()
result = self.handler(*args, **kwargs)
response: Response = self.app.make_response(result)
end = time.time()
time_in_ms = f"{(end-start)*1000:.2f}"
self.app.logger.info(
'%s %s %s user_agent="%s" referrer="%s" db_queries=%s %s '
+ "%s user_id=%s session=%s lang=%s %sms",
request.method,
request.url,
request.real_remote_addr,
request.user_agent,
request.referrer,
database.collect_query_count(),
response.status_code,
response.content_length,
session.get("user_id"),
session.id,
TranslationHandler.language,
time_in_ms,
)
return result
class ExceptionMiddleware(BaseMiddleware):
def __call__(self, *args, **kwargs):
try:
return self.handler(*args, **kwargs)
except (NotFound, PermissionDenied):
notFoundView = NotFoundView(**kwargs)
return notFoundView.render(), 404
class I18NMiddleware(BaseMiddleware):
def __call__(self, *args, **kwargs):
TranslationHandler.set_language(
session.get(
"lang",
TranslationHandler.parse_language_header(
request.headers.get("accept-language")
)
or "en",
)
)
return self.handler(*args, **kwargs)
def add_middleware_stack(app, *middlewares):
original_add_url_rule = app.add_url_rule
def url_rule_with_middlewares(
rule, endpoint=None, view_func=None, provide_automatic_options=None, **options
):
if view_func:
for middleware in middlewares[::-1]:
view_func = middleware(app, view_func)
return original_add_url_rule(
rule,
endpoint=endpoint,
view_func=view_func,
provide_automatic_options=provide_automatic_options,
**options,
)
app.add_url_rule = url_rule_with_middlewares