Repository navigation
Fix/kilo chat sandbox id migration tolerance #3308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,52 @@ | ||
| import { getWorkerDb } from '@kilocode/db'; | ||
| import { kiloclaw_instances } from '@kilocode/db/schema'; | ||
| import { and, eq, isNull } from 'drizzle-orm'; | ||
| import { and, eq, isNull, or } from 'drizzle-orm'; | ||
| import { | ||
| instanceIdFromSandboxId, | ||
| isInstanceKeyedSandboxId, | ||
| isValidInstanceId, | ||
| } from '@kilocode/worker-utils/instance-id'; | ||
|
|
||
| /** | ||
| * Half-migrated tolerance: a row's `sandbox_id` may still be the legacy | ||
| * userId-derived value while the kiloclaw DO has already moved to the | ||
| * ki_<uuid-hex> form (and the browser/bot path sends the latter). Derive | ||
| * the instance UUID that a ki_-form sandboxId references so callers can | ||
| * OR it into a `kiloclaw_instances.id` match. | ||
| * | ||
| * `isInstanceKeyedSandboxId` only checks prefix + total length, so a value | ||
| * like `ki_<35 chars of non-hex>` would pass through and format into a | ||
| * UUID-shaped string with non-hex characters — comparing that to a uuid | ||
| * column would make Postgres throw `invalid input syntax for type uuid`, | ||
| * turning a 4xx into a 500 on attacker-controlled input. Re-validate the | ||
| * derived UUID; if it fails, return null and skip the id-match branch. | ||
| */ | ||
| function candidateInstanceIdFromSandboxId(sandboxId: string): string | null { | ||
| if (!isInstanceKeyedSandboxId(sandboxId)) return null; | ||
| const derived = instanceIdFromSandboxId(sandboxId); | ||
| return isValidInstanceId(derived) ? derived : null; | ||
| } | ||
|
|
||
| async function queryOwnsSandbox( | ||
| connectionString: string, | ||
| userId: string, | ||
| sandboxId: string | ||
| ): Promise<boolean> { | ||
| const db = getWorkerDb(connectionString); | ||
| const candidateInstanceId = candidateInstanceIdFromSandboxId(sandboxId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: The test mock in Consider adding tests that cover:
The current test structure mocks at too high a level to observe which WHERE predicate was actually constructed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in b98318f |
||
| const rows = await db | ||
| .select({ sandbox_id: kiloclaw_instances.sandbox_id }) | ||
| .from(kiloclaw_instances) | ||
| .where( | ||
| and( | ||
| eq(kiloclaw_instances.sandbox_id, sandboxId), | ||
| eq(kiloclaw_instances.user_id, userId), | ||
| isNull(kiloclaw_instances.destroyed_at) | ||
| isNull(kiloclaw_instances.destroyed_at), | ||
| candidateInstanceId | ||
| ? or( | ||
| eq(kiloclaw_instances.sandbox_id, sandboxId), | ||
| eq(kiloclaw_instances.id, candidateInstanceId) | ||
| ) | ||
| : eq(kiloclaw_instances.sandbox_id, sandboxId) | ||
| ) | ||
| ) | ||
| .limit(1); | ||
|
|
@@ -27,11 +58,20 @@ async function querySandboxOwner( | |
| sandboxId: string | ||
| ): Promise<string | null> { | ||
| const db = getWorkerDb(connectionString); | ||
| const candidateInstanceId = candidateInstanceIdFromSandboxId(sandboxId); | ||
| const rows = await db | ||
| .select({ user_id: kiloclaw_instances.user_id }) | ||
| .from(kiloclaw_instances) | ||
| .where( | ||
| and(eq(kiloclaw_instances.sandbox_id, sandboxId), isNull(kiloclaw_instances.destroyed_at)) | ||
| and( | ||
| isNull(kiloclaw_instances.destroyed_at), | ||
| candidateInstanceId | ||
| ? or( | ||
| eq(kiloclaw_instances.sandbox_id, sandboxId), | ||
| eq(kiloclaw_instances.id, candidateInstanceId) | ||
| ) | ||
| : eq(kiloclaw_instances.sandbox_id, sandboxId) | ||
| ) | ||
| ) | ||
| .limit(1); | ||
| return rows[0]?.user_id ?? null; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: The secondary
isValidInstanceIdre-validation is correct and necessary, but the comment above it saysisInstanceKeyedSandboxId"only checks prefix + total length" — which is accurate. However, worth noting:instanceIdFromSandboxIdwill format any 32-character string into UUID shape regardless of whether those characters are valid hex.isValidInstanceIdthen re-checks with a strict hex UUID regex. This defense-in-depth is good.One edge case to be aware of: if
sandboxIdis exactlyki_followed by 32 non-hex chars (e.g.ki_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz),instanceIdFromSandboxIdreturns a UUID-shaped string with non-hex chars (e.g.zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz), andisValidInstanceIdcorrectly returnsfalse, sonullis returned. The guard works — just confirming the reasoning is sound.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing to fix here.