Files
max/max/app.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

70 lines
1.8 KiB
Python

import base64
import os
from flask import Flask
from werkzeug.routing import Rule as WerkzeugRule
from .auth import AuthMiddleware, init as init_auth
from .cli import register_cli
from .csrf import CsrfMiddleware
from .db import database
from .log import init_logging
from .middlewares import (
ExceptionMiddleware,
I18NMiddleware,
LoggingMiddleware,
add_middleware_stack,
)
from .routes import init_routes
from .sessions import SessionInterface
from .translations import TranslationHandler
class Rule(WerkzeugRule):
def __init__(self, *args, methods=None, **kwargs):
super().__init__(*args, **kwargs) # do not set methods
def main():
app = Flask(__name__)
init_logging(app)
app.logger.info("Max started.")
if app.debug:
app.logger.info("!! App is in debug mode !!")
else:
app.logger.info("App is in production mode")
TranslationHandler.init(app)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
app.url_rule_class = Rule
max_origin = os.environ.get("MAX_ORIGIN")
if not max_origin and not app.debug:
raise RuntimeError(
"MAX_ORIGIN must be set to the public origin, e.g. https://max.example.com"
)
app.config.update(
MAX_ORIGIN=max_origin.rstrip("/") if max_origin else None,
SECRET_KEY=(
"123456" if app.debug else base64.b64encode(os.urandom(64)).decode("utf-8")
),
SESSION_COOKIE_SECURE=not app.debug,
SESSION_COOKIE_SAMESITE=False,
)
add_middleware_stack(
app,
CsrfMiddleware,
AuthMiddleware,
I18NMiddleware,
LoggingMiddleware,
ExceptionMiddleware,
)
database.init(app)
app.session_interface = SessionInterface()
register_cli(app)
init_auth(app)
init_routes(app)
return app
app = main()