Skip to content

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

Description

@beardthelion

profiles.directoryFor in agent-computer/src/profiles.ts:118 is join(root, botId) with no confinement, and reset() runs rm(directoryFor(botId), { recursive: true, force: true }) as root. join normalizes .., so an id of ../workspace resolves outside the profiles root.

The id is not an internal value. It comes from the URL, and the shared (non-supervisor) deployment carries it end to end:

  1. server/src/computer/routes.ts:215 takes :botId behind requireUser and nothing else, no validation.
  2. server/src/computer/gateway.ts:433 calls as(botId).resetComputer().
  3. server/src/computer/client.ts:187 sends it verbatim as x-openbot-bot-id, and attaches the server's own x-openbot-computer-token. The caller never needs that token.
  4. agent-computer/src/index.ts:137 botIdOf trims it; :636 routes /computers/reset to profiles.reset(botId).

So POST /api/computers/..%2Fworkspace/computers/reset from an account holding only the base user role deletes the durable workspace volume. A bare .. is normalized away by the router; the percent-encoded form is not.

Verified at main 06a1a84, both halves separately.

Driving the real Hono routes with a role-user actor and a recording fetch, so the front half is observed rather than argued:

URL segment HTTP on the wire as x-openbot-bot-id
sales 200 sales
..%2Fworkspace 200 ../workspace
..%2F..%2Fetc 200 ../../etc

Then the real createProfiles against a sandboxed tree. stop() returns early when no browser is running, so reset reaches the rm without Chromium:

reset(botId) deletes
sales /profiles/sales, correctly confined
support /profiles/support, correctly confined
../workspace the workspace volume
../etc a sibling of the profiles root
../../above two levels up
sales/../../etc a sibling of the profiles root
/etc /profiles/etc, confined, since join treats it as relative

Deletion is the sharp end, but page() passes the same unconfined path to launchPersistentContext, so Chromium also writes a user-data directory wherever the caller points.

Two things bound it. The supervisor deployment is unaffected: supervisor/src/names.ts holds ids to a strict allow-list, and running these same inputs through namesFor refuses every one of them. And agent-computer/src/workspace.ts confines file paths in three layers, so the workspace tools were never the way in. The profile path is the one place an id becomes a path with no check at all, and there is no test file for profiles.ts.

A PR follows that applies the rules names.ts already uses, at the computer's own request boundary and where the path is built.

Worth noting separately, since it is out of scope for that PR: 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions