login: honour next on both exits, same-origin only

The console's 401 redirect carries next=/leaders/ and /login dropped it, so a
leader arriving cold landed on /account. next now rides the form as a hidden
field and is applied after a successful POST and when an already-signed-in
person hits /login. identity.safe_next() admits only a relative path with a
single leading slash - no scheme, no //, no backslash, no control characters -
so the login page cannot become an open redirect. Ten checks added to
tests/smoke_identity.py; the suite is 59 + 24 + 53, all green.
This commit is contained in:
2026-09-04 16:28:58 -04:00
parent 32e1c67a6f
commit 5c7a8dd785
3 changed files with 38 additions and 6 deletions
+14
View File
@@ -606,6 +606,20 @@ def consume_invite(token, full_name, password, preferred_name=None, phone=None,
# Rows, not signed cookies. A leader stepping down has to be revocable now
# rather than at token expiry, and "sign out everywhere" has to be possible.
def safe_next(value, default="/account"):
"""Where to send someone after login. Only a same-origin relative path
survives: a single leading slash, no scheme, no protocol-relative `//`,
no backslash or control character that a browser might normalise into
one. Anything else falls back to default. This is what keeps /login from
being an open redirect the moment it starts honouring `next`."""
v = (value or "").strip()
if not v.startswith("/") or v.startswith("//") or v.startswith("/\\"):
return default
if any(ord(c) < 32 or c in "\\" for c in v):
return default
return v
def start_session(person_id, ip=None, user_agent=None):
tok = secrets.token_urlsafe(32)
con = connect()