Skip to content

fix(web): exchange the launch token for a short-lived session (F-04) - #51

Merged
404SecNotFound merged 1 commit into
mainfrom
fix/review-f04-session-exchange
Aug 3, 2026
Merged

fix(web): exchange the launch token for a short-lived session (F-04)#51
404SecNotFound merged 1 commit into
mainfrom
fix/review-f04-session-exchange

Conversation

@404SecNotFound

Copy link
Copy Markdown
Owner

The launch token is persistent, lives in ~/.config/replicant/web-token, and was
ALSO the value written into the session cookie. So the cookie was the master
credential: no expiry, no rotation, and no way to revoke one browser without
regenerating the token file and breaking every script and every other client at
the same time. Anything that read that cookie held permanent access.

The same token was additionally appended to EventSource and WebSocket URLs,
which is the least private part of a request: URLs reach server logs, browser
history and the Referer header.

Both halves are gone.

SessionStore issues short-lived random ids (12h), validates, expires,
revokes one or all, and sweeps on issue so a reconnect loop cannot grow it
without bound. The cookie now holds one of those. It is per app rather than
module-global, so two apps in one process cannot authenticate each other's
browsers. Secure is set only over https, because setting it on the loopback
http this tool serves by default would stop the cookie being sent at all.

POST /api/session/logout revokes this browser and nothing else. Deliberately
unauthenticated: the worst anyone can do is end a session they already hold.

The query parameter is gone from both stream URLs. EventSource and WebSocket
cannot set headers, which is why the token was there, but both are same-origin
and the cookie is already set by the page load that precedes them.

The launch token still works as a header for non-browser clients, because a
script cannot run a cookie jar.

Five tests in test_web_access.py wrote the launch token straight into the cookie,
which is exactly what this removes. They now establish a real session through the
server, so they still test cookie auth and the Origin rules, but against a
credential the server minted.

Verified in a browser, not only in tests: address bar carries no token,
document.cookie is empty (httpOnly), and the HTTP API, the terminal WebSocket
and the run SSE stream all authenticate on the cookie alone, with CEF lines
arriving over SSE.

853 py + 136 fe. black, ruff, mypy clean. All 14 new guards observed to fail first.

The launch token is persistent, lives in ~/.config/replicant/web-token, and was
ALSO the value written into the session cookie. So the cookie was the master
credential: no expiry, no rotation, and no way to revoke one browser without
regenerating the token file and breaking every script and every other client at
the same time. Anything that read that cookie held permanent access.

The same token was additionally appended to EventSource and WebSocket URLs,
which is the least private part of a request: URLs reach server logs, browser
history and the Referer header.

Both halves are gone.

**SessionStore** issues short-lived random ids (12h), validates, expires,
revokes one or all, and sweeps on issue so a reconnect loop cannot grow it
without bound. The cookie now holds one of those. It is per app rather than
module-global, so two apps in one process cannot authenticate each other's
browsers. `Secure` is set only over https, because setting it on the loopback
http this tool serves by default would stop the cookie being sent at all.

**POST /api/session/logout** revokes this browser and nothing else. Deliberately
unauthenticated: the worst anyone can do is end a session they already hold.

**The query parameter is gone from both stream URLs.** EventSource and WebSocket
cannot set headers, which is why the token was there, but both are same-origin
and the cookie is already set by the page load that precedes them.

The launch token still works as a header for non-browser clients, because a
script cannot run a cookie jar.

Five tests in test_web_access.py wrote the launch token straight into the cookie,
which is exactly what this removes. They now establish a real session through the
server, so they still test cookie auth and the Origin rules, but against a
credential the server minted.

Verified in a browser, not only in tests: address bar carries no token,
`document.cookie` is empty (httpOnly), and the HTTP API, the terminal WebSocket
and the run SSE stream all authenticate on the cookie alone, with CEF lines
arriving over SSE.

853 py + 136 fe. black, ruff, mypy clean. All 14 new guards observed to fail first.
@404SecNotFound
404SecNotFound merged commit 314c329 into main Aug 3, 2026
10 checks passed
@404SecNotFound
404SecNotFound deleted the fix/review-f04-session-exchange branch August 3, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant