Commit Graph
16 Commits
Author SHA1 Message Date
finnandClaude Opus 5.5 8e32727fcd Limit sessions per user instead of a global LRU cap
The session store held at most 100 sessions and evicted the least
recently used one. Anyone with an account could log in 101 times and
thereby log out every other user, including the admins.

- Each user has at most 10 sessions. A new login above the limit
  removes the user's own least recently used session (by _last_seen).
- There is no global limit anymore: the store only accepts sessions
  with a user_id (raises SessionWithoutUser otherwise), so it is
  bounded by 10 * number of users. Anonymous users still create no
  store entries.
- save_session clears an invalid session before re-raising, since
  Flask saves the session again for the error response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 17:27:44 +00:00
finnandClaude Opus 5.5 80757fa1d8 Fix HTTP 500 on missing email or domain when creating users and aliases
If the email or domain field was missing or invalid, the error was
flashed, but the address was still built from the unset attributes,
which raised an AttributeError. Only build and validate the address
if both are set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:25:38 +00:00
finnandClaude Opus 5.5 4a03b5c1a1 Reserve privileged alias addresses for admins
Any user could create aliases like postmaster@, webmaster@ or abuse@ on
their domain. CAs use these addresses to validate domains, so a family
account (or a compromised one) could obtain TLS certificates or catch
abuse and system mails.

- Only admins may create aliases with a reserved local part (admin,
  administrator, webmaster, hostmaster, postmaster, abuse, security,
  noc, root, mailer-daemon). The acting user counts, so admins can
  still assign such aliases to other users.
- Escape the domains in the email regex, so "finn.st" no longer
  matches "finnXst".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:23:12 +00:00
finnandClaude Opus 5.5 fc24ba068d Prevent session fixation on login
login() kept the existing session id and CSRF token. Anyone who knew
the id before (e.g. via cookie injection) was logged in as the victim
afterwards and knew the CSRF token, too.

- login() clears the session after a successful check. Since the fix of
  Session.clear() this deletes the old store entry, drops the CSRF
  token, resets the timestamps and assigns a new id.
- open_session no longer adopts signed ids without a store entry, so an
  id chosen by the client never becomes a session id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:51:35 +00:00
finnandClaude Opus 5.5 cc3d064e64 Expire sessions after 30 min idle and 12 h absolute
Sessions were only bounded by a process restart or the LRU eviction, so
a stolen or fixed session id could stay valid for months. A cookie TTL
would not help, since an attacker with a copy of the id ignores it.

The store entry now carries _created_at and _last_seen (monotonic
time). They are kept out of the session dict as attributes of the
Session object, so is_empty() and the app code are unaffected.
open_session drops expired entries and returns a fresh, empty session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:46:44 +00:00
finnandClaude Opus 5.5 ca023e705c Invalidate sessions server-side on logout and deactivation
Session.clear() set the id to None before clearing, so on_update
assigned a new id and save_session deleted the store entry of that new
id. The entry of the old id with the user_id stayed valid: a copied
cookie survived the logout, and a session "logged out" by the auth
middleware for a disabled user was valid again after re-enabling.

- Session.clear() now removes the store entry of the current id.
- Deactivating a user removes all of their sessions immediately, like
  deleting a user does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:03:03 +00:00
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
finnandClaude Opus 5.5 91e95b1947 Make isort compatible with black
isort's lines_after_imports = 2 conflicted with black, so run-cleanup
reverted black's changes. Use isort's black profile instead and apply the
resulting formatting. Also fix the flake8 errors in check_email_mixin and
drop a leftover debug print.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 09:06:51 +00:00
finnandClaude Opus 5.5 340006ff37 Disable debug default login
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:39:28 +00:00
finnandClaude Opus 5.5 86fcdfde27 Devcontainer: enable bash-completion and set UTF-8 locale
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:39:28 +00:00
finnandClaude Opus 5.5 e0b5e05cce Use port 8757 for the dev server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:39:28 +00:00
finnandClaude Opus 5.5 9f25457b24 Remove old docker-compose based dev setup
The devcontainer replaces Dockerfile.dev, dc.dev.yml and the related
start/entrypoint scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:39:28 +00:00
finnandClaude Opus 5.5 9895017385 Restructure scripts into dev/ and prod/ for the devcontainer
- scripts/prod: entrypoint, db-setup and schema.sql shipped in the image
- scripts/dev: seed/clear db, psql and cleanup, run natively in the
  devcontainer instead of via docker-compose
- move setup.cfg to the repo root so linters pick it up
- Makefile targets run directly in the devcontainer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:39:28 +00:00
finnandClaude Opus 5.5 7c2b9fb3f4 Refactor logging and drop deprecated FLASK_ENV
Move logging setup into max/log.py. The file handler is only used outside
debug mode and its directory is configurable via LOG_DIR. FLASK_ENV is
deprecated in Flask; debug mode is now controlled via FLASK_DEBUG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:21:07 +00:00
finn 65dddea3bc initial switch to a devcontainer 2026-10-21 06:25:18 +00:00
finn 2bd853e9b5 modernizing the dependencies 2026-10-19 18:53:59 +02:00