Skip to content

Keep a Bot id from naming a directory it should not - #30

Open
beardthelion wants to merge 1 commit into
CopilotKit:mainfrom
beardthelion:fix/confine-bot-id-profile-paths
Open

Keep a Bot id from naming a directory it should not#30
beardthelion wants to merge 1 commit into
CopilotKit:mainfrom
beardthelion:fix/confine-bot-id-profile-paths

Conversation

@beardthelion

Copy link
Copy Markdown

Closes #29.

profiles.directoryFor was join(root, botId), which normalizes .. away, so a Bot id of ../workspace resolved outside the profiles root and reset deleted whatever was there with rm -rf, as root. The id comes from the URL, and every acting route under /api/computers/:botId requires 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/reset from an ordinary account was enough.

What it does

Holds a Bot id to the rules 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.

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. /health stays 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 split authorisation.ts already makes, and for the same stated reason.

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. names.ts validates 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 createProfiles on a sandboxed tree, before and after. Before, reset("../workspace"), reset("../etc"), reset("../../above") and reset("sales/../../etc") each deleted a directory outside the profiles root. After, each is refused and every file is still there, while reset("sales") still deletes exactly that profile.

Not covered by a test: the guard in index.ts itself. That file calls serve() 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 as main (integration tests that want a Postgres). Typecheck and biome clean.

Not in scope

No acting route resolves :botId against a row in bots, 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.

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.
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.

A bot id from the URL becomes a filesystem path, so any signed-in user can delete directories in the computer container

1 participant