Keep a Bot id from naming a directory it should not - #30
Open
beardthelion wants to merge 1 commit into
Open
Conversation
A Bot id arrives as a request header, and the API server forwards whatever segment a caller put in the URL. `profiles.directoryFor` was `join(root, botId)`, which normalizes `..` away, so `../workspace` resolved outside the profiles root and `reset` deleted whatever was there with `rm -rf`, as root. Reached from the product, not only from a leaked computer token: every acting route under `/api/computers/:botId` requires a signed-in user and nothing more, and the server attaches its own token when it forwards the id. So `POST /api/computers/..%2Fworkspace/computers/reset` from an ordinary account deleted the durable workspace volume. A bare `..` is normalized away by the router; the encoded form is not. The rules are the ones `supervisor/src/names.ts` already applies to container and volume names, for the same reason and in the same words: letters, digits, hyphen and underscore, starting with a letter or digit. No separators, so an id cannot escape into another path segment; no dots, which invite `..` reasoning. The check sits in its own file rather than in `index.ts`, which imports Playwright at module scope, so it can be tested without a browser, the same split `authorisation.ts` already makes. Refused at the computer's own request boundary as well as where the path is built. The header is this process's input, so it holds the line itself rather than trusting the caller to have checked. `/health` stays exempt, as it is from the token, because it names no Bot and an orchestrator's probe should not fail on a header it never meant to send. `COMPUTER_BOT_ID` is validated at boot rather than per request. It is the id every unheadered call falls back to, so a value these rules refuse would answer 400 to everything and read as a broken computer.
beardthelion
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
August 20, 2026 03:29
2 tasks
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 #29.
profiles.directoryForwasjoin(root, botId), which normalizes..away, so a Bot id of../workspaceresolved outside the profiles root andresetdeleted whatever was there withrm -rf, as root. The id comes from the URL, and every acting route under/api/computers/:botIdrequires a signed-in user and nothing more, with the server attaching its own computer token when it forwards the id.POST /api/computers/..%2Fworkspace/computers/resetfrom an ordinary account was enough.What it does
Holds a Bot id to the rules
supervisor/src/names.tsalready applies to container and volume names, for the same reason and in the same words: letters, digits, hyphen and underscore, starting with a letter or digit. No separators, so an id cannot escape into another path segment; no dots, which invite..reasoning.Enforced in two places. Where the path is built, so there is one checked way to name a profile directory, and at the computer's own request boundary, because the header is this process's input and it should not depend on the caller having checked.
/healthstays exempt, as it already is from the token, since it names no Bot and an orchestrator's probe should not fail on a header it never meant to send.The check lives in its own file rather than in
index.ts, which imports Playwright at module scope, so it can be tested without a browser. That is the splitauthorisation.tsalready makes, and for the same stated reason.COMPUTER_BOT_IDis validated at boot rather than per request: it is the id every unheadered call falls back to, so a value these rules refuse would answer 400 to everything and read as a broken computer.names.tsvalidates its namespace the same way.Verification
27 cases in
agent-computer/tests/bot-id.test.ts, failing before the change and passing after: the traversal forms, and the ordinary ids that have to keep working (sales,support-1,risk_analyst,shared, a uuid-shaped id, a single character).The behaviour itself was exercised against the real
createProfileson a sandboxed tree, before and after. Before,reset("../workspace"),reset("../etc"),reset("../../above")andreset("sales/../../etc")each deleted a directory outside the profiles root. After, each is refused and every file is still there, whilereset("sales")still deletes exactly that profile.Not covered by a test: the guard in
index.tsitself. That file callsserve()at module scope, so no test can import it, which is the same reason its routes have no tests today. The logic it calls is what the 27 cases cover.agent-computer: 118 pass, 0 fail.server: same failure set asmain(integration tests that want a Postgres). Typecheck and biome clean.Not in scope
No acting route resolves
:botIdagainst a row inbots, so any signed-in user can act on any Bot's computer even with a well-formed id. That is a separate question from path confinement and is noted at the end of #29.