Repository navigation
Console: serve the static shell past the remote lifetime to its own session (#1006) - #1008
Merged
Merged
Conversation
…ession (#1006) On `keel serve --external-host`, a browser with no cached console was refused the page itself after the 12 h lifetime (#648), so it never reached the expired-Pulse HALT that #985 serves. A GET/HEAD of a shell path -- a shipped file under the static root or a client route, by the same resolution `_serve_static` performs -- carrying THIS session's cookie now skips the lifetime (`KeelHandler._is_a_sessioned_shell_read`, called from `_admitted`; `do_GET` is untouched). The shell is the package's files unaltered: no data, no token. Every `/api/` read (events included), `/switch/`, unmapped paths and every write keep the lifetime; a request without the cookie is refused the shell as before, so an expired session is never more open to a stranger than a live one. The token exchange already ran before admission, so a first visit via the printed address gets the cookie and then the page. Docstrings, docs/remote-access.md and the prose test that used `/` to show the lifetime are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
commented
Oct 14, 2026
eaitbrahim
left a comment
Contributor
Author
There was a problem hiding this comment.
Round-1 review. One test gap (low). The exemption itself holds: I probed //api/pulse, /api%2fpulse, /%61pi/pulse, /%2e%2e/..., ..%2f, //switch/8766, /api, /switch, trailing slashes, query strings, ?token=x and README.md past the lifetime with GET and HEAD, with and without the cookie. Nothing outside the shell is admitted, and every cookieless answer is the plain Session expired.
…quest (#1006) CodeQL counted a second user-controlled flow into resolve_static_asset. _is_a_sessioned_shell_read now compares the unquoted request path against staticfiles.shell_request_paths, enumerated from the package side, so the request path never reaches a filesystem call. Adds the HEAD pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1006
What changed
On
keel serve --external-host, past the 12 h session lifetime (#648), a GET/HEAD of the client shell carrying this session's cookie is now served instead of refused, so a browser with no cached console loads the page and reaches the expired-Pulse HALT that #985 added.KeelHandler._is_a_sessioned_shell_read(new), consulted inside_admitted.do_GETis untouched (CodeQL alerts point at it).staticfiles.shell_request_paths(STATIC_ROOT), a list built from the package side: each shipped file with a known Content-Type,/, and everyCLIENT_ROUTES/RETIRED_CLIENT_ROUTESname. The request path never reaches a filesystem call (that keeps CodeQL from seeing a second path-injection flow), and non-canonical spellings (/js/../index.html,/pulse/) are not exempted, so they're refused as expired. Anything under/api/(events included) and/switch/is excluded by prefix first, so a file planted under the static root cannot reopen one./api/*: every read and write keeps the lifetime except Remote session lifetime (#648) refuses HALT too: a keel serve --external-host console can't halt after 12h until restarted #983 (tokened halt POST) and Console: keep the HALT control on Pulse after the remote session lifetime expires (client half of #983) #985 (cookie-matched expired/api/pulsecarries{halt_token, resume_command}). The gates token stays served nowhere (Console: the HALT token served on /api/pulse also releases resume routes #968). The shell is the package's files byte for byte, with no template, so there's no data and no token in it.server,security.REMOTE_SESSION_MAX_AGE_SECONDS,gates.HALT,staticfiles.resolve_client_route),docs/remote-access.md(now "three exceptions, all for the stop"), and the prose test that used/to demonstrate the lifetime (now/api/config).Ruling (the issue's held question)
Ruling: the shell is served past the lifetime to this session's cookie only, not to any browser. Why: before the lifetime a cookieless request is refused the shell (
test_static_assets_are_behind_the_same_admission_as_every_other_page: "not exempted ... just because it holds no secrets today"). Serving it to anyone after expiry would make an expired server more open to strangers than a live one. A cookieless browser gains nothing anyway, because the Pulse refusal draws HALT only for the cookie. A first visit through the printed?token=address gets the cookie from the exchange, which runs before admission, so it's covered. The issue's premise that "a loopback server already serves it freely" doesn't hold: loopback gates the shell on the cookie too. Cost if wrong: a cookieless browser past the lifetime keeps getting the plain-text "Session expired" (which nameskeel kill) instead of a page that couldn't halt either.Tests (TDD: written first, failed with 403 "Session expired")
/?token=past the lifetime → 303 + cookie → shell 200 →/api/pulse403 carrying the halt token./api/events,/api/config,/api/pulse/,/switch/8766and unmapped paths stay "Session expired".api/pulse.json,api/events.js,switch/8766.htmlandkeel.binreopens none of them, and a POST to a shell path keeps the lifetime.Verified (final head)
mypyclean. No JS changed;node --checkis clean on all static JS anyway./?token=loads the page, Pulse shows "Session expired" with HALT, the HALT click engages, and amode=roread of the temp DB giveskill_switch|true.🤖 Generated with Claude Code