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>
70 lines
1.8 KiB
Python
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()
|