Repository navigation
fix(console): end console sessions 8 hours after login - #875
Conversation
A console session's idle timeout is refreshed by every request that passes its check, so a session token used at least every 8 hours stayed valid until the process restarted. The cookie's Max-Age does not bound it: the server never sees that attribute, and a stolen token can be sent without it. Record when each session was created and end it 24 hours after login, however active it is, in addition to the 8-hour idle timeout. Lookup and the insert-time prune apply both limits. Activity refreshes only the idle timer, never the login time.
LeeroyHannigan
left a comment
There was a problem hiding this comment.
The cap works as described, but at 24 hours a stolen token stays valid for 16 hours after the browser drops the cookie.
This PR stores the login time of each console session. The server now refuses a session 24 hours after login, and the 8 hour idle timeout still applies. Only create writes created_at, at login, and touch writes only last_active, so no request can reset the login time. lookup and the insert-time prune both call is_expired. Logout removes the entry, and a restart empties the in-memory store.
Both limits read Instant, so setting the system clock does not move them. I tested the edge cases of is_expired in a scratch test. A session exactly 24 hours old is still valid, and the check refuses a session one nanosecond older. The idle check uses the same > comparison. With a now earlier than the stored instants, or with Duration::MAX limits, is_expired returns false and does not panic.
I ran the server on 127.0.0.1 under libfaketime, which also moves the monotonic clock that Instant reads. On this branch, a token sent at 7, 14 and 21 hours and at 23 h 59 min after login got 200 each time. At 24 h 1 min, with 2 minutes idle, it got the redirect to /console/login, and so did the next request. On main, the same requests got 200 at 24 h 1 min and at 31 hours. That session ended only after 8 h 1 min idle. After a restart, the token from the earlier process got the login redirect.
On Rust 1.88.0, cargo test -p extenddb-server --lib console passes 38 of 38. With session.rs reverted to main, the test module does not compile (8 errors). I broke each part of the change in turn, and each new test fails against its part:
is_expiredwithout the lifetime clause: the two lifetime tests, as the description sayslookupwith the old idle-only check:an_active_session_ends_at_its_maximum_lifetime_and_stays_ended- the prune with the old idle-only
retain:creating_a_session_prunes_sessions_past_their_maximum_lifetime - a
touchthat also resetscreated_at:activity_does_not_extend_the_maximum_lifetime SESSION_MAX_LIFETIMEat 25 hours:sessions_last_at_most_twenty_four_hours_after_login
cargo +1.97.0 fmt --all -- --check and cargo +1.97.0 clippy -p extenddb-server --all-targets -- -D warnings are clean. Every CI check passes except Analyze (rust).
Findings
Major, crates/server/src/console/session.rs:38. The login response sets Max-Age=28800 (crates/server/src/console/pages/auth_pages.rs:61), and on the running server no later response set the cookie again. A browser therefore drops the cookie 8 hours after login, however active the session is. After that, only a client that ignores Max-Age can use the session, for example one that holds a stolen token. The 24 hour cap gives that client 16 more hours. Set SESSION_MAX_LIFETIME to 8 hours, the time at which the browser drops the cookie, so browser users see no change. Build the cookie's Max-Age from the same constant. With an 8 hour cap, the idle check never ends a session first. A session idle for 8 hours is also 8 hours past login.
Minor, docs/getting-started.md:1284 and docs/manuals/05-admin-guide.md:832. These guides describe the console in a browser, and they now say that an active session lasts up to 24 hours. In a browser the session ends 8 hours after login, when the cookie expires. With an 8 hour cap, write "Sessions end 8 hours after login." If the cap stays at 24 hours, add that the browser drops the cookie 8 hours after login. The architecture guide and the security model describe the server, and their lines match the code.
Minor, docs/troubleshooting.md:857. The cause given for "Console redirects to login page on every request" is still "sessions last 8 hours of inactivity". A user reads this entry after the cap ends a session. Add the cap and the cookie's 8 hour lifetime to that cause.
Optional, crates/server/src/console/session_tests.rs:315. The prune test covers only the lifetime half of is_expired. A prune that checks only created_at passes all 13 session tests. Add a session past the idle timeout to this test. Assert that the prune removes it.
Not checked
- Why
Analyze (rust)(CodeQL) failed. The job stopped in its upload step with no error message, and there is no open alert for this PR. The same job passed on main and on nine other PRs on 5 November. A re-run will show whether it fails again.
The login cookie's Max-Age is 8 hours and is never refreshed, so a browser drops it 8 hours after login however active the session is. A 24-hour server cap only gave a client that ignores Max-Age, such as one holding a stolen token, 16 more hours. Cap sessions at 8 hours after login and build the cookie's Max-Age from the same constant, so the browser and the server end a session at the same time. The 8-hour idle check stays; with the two equal, the lifetime ends every session first. Describe the 8-hour lifetime in the user guides and the troubleshooting entry for login redirects, and extend the insert-time prune test to cover an idle session as well as an outlived one.
LeeroyHannigan
left a comment
There was a problem hiding this comment.
b449df0 fixes the four findings from my last review, so I approve.
- The cap is now 8 hours. The login cookie's
Max-Agecomes from the same constant,SESSION_MAX_LIFETIME_SECS(crates/server/src/console/session.rs:40, used atcrates/server/src/console/pages/auth_pages.rs:61). docs/getting-started.md:1284anddocs/manuals/05-admin-guide.md:832now say that sessions end 8 hours after login. The entry atdocs/troubleshooting.md:857gives the lifetime and the cookie as the cause.creating_a_session_prunes_idle_and_outlived_sessionsnow holds one session past the lifetime and one past the idle timeout. When the prune checks only the login time, the test fails at line 354. When the prune checks only the idle time, it fails at line 353.
On Rust 1.88.0, the 13 session tests pass at this head.
Summary
Each console request that passes its session check resets the 8-hour idle timeout. A session token used at least every 8 hours therefore stayed valid until the server process restarted. The cookie's
Max-Age=28800does not bound this on the server side: the server never sees that attribute, and a stolen token can be sent without it.This change records when each session was created and ends the session 8 hours after login, however active it is. The login cookie's
Max-Ageis built from the same constant, so the browser drops the cookie at the moment the server ends the session.Follow-up to #704 (sessions end when the principal is deleted or its password changes) and #773 (
Securecookie flag).Changes
crates/server/src/console/session.rsSESSION_MAX_LIFETIME_SECS(8 h,pub(crate)) andSESSION_MAX_LIFETIME, plusSessionEntry.created_at.SessionEntry::is_expired()checks both limits. Lookup uses it, and so does the prune that runs on insert when the store is full.last_activeonly, nevercreated_at.with_ttlis replaced bywith_limits(ttl, max_lifetime).crates/server/src/console/pages/auth_pages.rs: the login cookie'sMax-AgeisSESSION_MAX_LIFETIME_SECSinstead of the literal28800.crates/server/src/console/session_tests.rsadds four tests:created_atunchanged;Behaviour
Testing
cargo test -p extenddb-server --lib console: 38/38 pass.is_expired: the two lifetime tests;created_at: the prune test;cargo +1.97.0 fmt --all -- --checkandcargo +1.97.0 clippy -p extenddb-server --all-targets -- -D warningsare clean.