Skip to content

The body of a human-input request picks which computer endpoint the server calls #110

Description

@beardthelion

server/src/computer/routes.ts:310 checks :kind against the four gestures a person's mouse and keyboard produce, and then builds the call to the gateway as:

await gateway.humanInput(context.req.param("botId"), {
  kind,
  ...(body ?? {}),
} as Parameters<typeof gateway.humanInput>[1]),

The spread comes second, so a body carrying its own kind replaces the value that was just checked. humanInput (gateway.ts:625) puts that value straight into the path it calls:

const { kind, ...payload } = input;
return post<HumanInputResult>(botId, `/human/${kind}`, payload);

and the transport concatenates it onto the computer's address (client.ts:129), so URL parsing inside fetch resolves the .. away.

Reproduced

Against a listening server standing in for agent-computer, driving the real router through the real gateway and transport at main 37b576c. The paths below are what the server received:

POST to /bot-1/human/click with body arrived at
{"x":10,"y":20} /human/click
{"kind":"../computers/reset"} /computers/reset
{"kind":"../exec","command":"cat /workspace/notes"} /exec, body {"command":"cat /workspace/notes"}
{"kind":"../../health"} /health

client.ts:134 attaches x-openbot-computer-token to everything the transport sends, so each of those arrives authenticated. /computers/reset wipes the profile and every login in it; /exec runs a command on the Bot's computer.

Why this route in particular

Three defences are stepped around rather than defeated, because the request never enters the paths that carry them:

  • The policy decision and the audit row. This route skips both deliberately (routes.ts:305-308) and correctly: a takeover exists so a person can enter a password, and recording keystrokes would defeat the point. It does mean a redirected call leaves nothing behind that says where it went.
  • The computer's own humanMayDrive() check (agent-computer/src/index.ts:624) is keyed to the four /human/* paths. A request routed elsewhere never reaches it, so the takeover requirement does not apply to it.

Who can reach it

The only gate is the Bot-access middleware every route under /:botId/* has (routes.ts:59). There is no requireAdmin. Since canRunAgent is canAccessAgent, that is any signed-in person who may act as the Bot, and for a Bot whose visibility is public that is everyone in the deployment.

Test plan

  • Send {"kind":"../computers/reset"} to /human/click for a Bot you can access. Expect the profile to survive.
  • Send {"kind":"../exec","command":"id"}. Expect nothing to run.
  • Take control and click, type, press a key and scroll. Expect all four to work as before.

A PR follows.

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