Skip to content

Refuse a scroll whose deltaY is not a finite number - #452

Open
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:fix/scroll-delta-validation
Open

Refuse a scroll whose deltaY is not a finite number#452
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:fix/scroll-delta-validation

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What is wrong

Both scroll routes guard deltaY with a bare typeof check:

routes.post("/:botId/scroll", (context) =>
  act(context, (botId, actor, body) =>
    gateway.scroll(botId, actor, {
      ...(typeof body?.deltaY === "number" ? { deltaY: body.deltaY } : {}),
    }),
  ),
);

typeof Infinity === "number", and Infinity is reachable from the wire — 1e999 is valid JSON:

$ bun -e 'console.log(JSON.stringify({ deltaY: 1e999 }), JSON.parse(String.raw`{"deltaY":1e999}`).deltaY)'
{"deltaY":null} Infinity

So the value passes the check, reaches the gateway, and is written as null by JSON.stringify on
the hop to the Bot's computer. There, typeof body.deltaY === "number" is now false, and
agent-computer/src/index.ts falls back to its own default — 600 pixels on /scroll, 400 on
/human/scroll. The caller is answered 200 with a scroll it did not ask for.

POST /:botId/human/:kind is worse in one respect: it spreads the whole body through with no
validation at all, so nothing on that path looks at deltaY before the computer does.

Why this is the odd one out

Every other number on this surface is already checked at the edge:

  • timeoutMs on exec is refused unless it is a whole number between 1000 and 600000 (Validate timeoutMs on POST /computers/:botId/exec #427), and
    that check explicitly rejects Infinity, null and "3000".
  • The coordinates behind human/click are refused unless Number.isFinite accepts them.

deltaY was the remaining one. This checks it the same way, in the same place, with the same 400.

The change

A shared usableDeltaY predicate — absent, or Number.isFinite — used by /:botId/scroll and by
the scroll case of /:botId/human/:kind. Wrong types are refused rather than silently dropped,
matching what exec already does with a timeoutMs of "3000".

Verification

bun test server/tests/computer-scroll-delta.test.ts, both paths covered by describe.each:

  • With server/src/computer/routes.ts reverted to main and only the new test applied:
    10 fail, 4 pass. Every rejection case on both routes answers 200 instead of 400 and the
    fake gateway records the call.
  • With the change: 14 pass, 0 fail.

The test posts the body as raw text rather than a stringified object, because
JSON.stringify({ deltaY: Infinity }) is {"deltaY":null} and cannot express what a client actually
sends.

Also green: bun test server/tests/computer-routes.test.ts server/tests/computer-exec-timeout.test.ts server/tests/computer-policy-route.test.ts server/tests/computer-fleet-route.test.ts (26 pass),
bun run --filter server typecheck, bunx biome check.

Note

The CHANGELOG.md entry lands at the top of ## Unreleased, the same anchor as #450, so whichever
merges second needs a one-line rebase. Happy to do it on request.

`POST /:botId/scroll` and `POST /:botId/human/scroll` guarded `deltaY` with
`typeof value === "number"`, which is true of `Infinity`. `1e999` is valid JSON
and parses to exactly that, so the value passed the check, travelled to the
gateway, and was written by `JSON.stringify` as `null` on the hop to the Bot's
computer -- where `typeof body.deltaY === "number"` is now false and the
computer scrolls its own default instead (600 pixels for the tool path, 400 for
a person's). The caller got 200 and a scroll it did not ask for.

Every other number on this surface is already checked at the edge: `timeoutMs`
on `exec` is rejected unless it is a whole number in range (CopilotKit#427), and the
coordinates behind `human/click` are rejected unless `Number.isFinite` accepts
them. `deltaY` was the one that was not, so it is checked the same way, at the
same place, with the same 400.

Wrong types are refused rather than silently dropped, which is what `exec`
already does with a `timeoutMs` of `"3000"`.

Measured: with the routes unchanged and only the new test applied, 10 of 14
cases fail -- every rejection case on both paths answers 200 and reaches the
gateway. With the change, 14 pass.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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