Security [HIGH]: Read-only proxy bypass via tRPC callers + missing $transaction/$queryRaw blocks #47
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
read-only-prisma.tsonly blocks$executeRaw[Unsafe], not$transaction,$queryRaw[Unsafe],$runCommandRaw. Tools in helpers.ts pass rawctx.dbtocreateScopedCallerContext→ tRPC callers bypass the proxy and can mutate. Separately, several tool executors callassertPermissionAFTER input parsing/DB lookups, allowing enumeration before auth.Evidence
packages/api/src/lib/read-only-prisma.ts:47-52 — only $executeRaw[Unsafe] blockedpackages/api/src/router/assistant-tools/helpers.ts:1676-1680 — raw ctx.db passed to scoped callerpackages/api/src/router/assistant-tools/advanced-timeline.ts:617 — assertPermission lateImpact
LLM-invoked 'read-only' tool can trigger writes via tRPC-caller indirection, violating the read-only guarantee. Late
assertPermissionleaks info via lookup errors before auth fires.Proposed Fix
(1) Extend proxy to block
$transaction,$queryRaw*,$runCommandRaw. (2)createScopedCallerContextmust propagateisReadOnlyand swap in the proxy so tRPC callers see it. (3) MoveassertPermission(ctx, PermissionKey.XXX)to the FIRST line of every tool executor, before argument parsing. (4) Unit test: each read-tool invoked with mutation-intent payload → no writes observed.Acceptance Criteria
Parent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (C-3, C-4)
Resolved.
Part 1 — Proxy blocks all escape hatches (commit
1ff5c33, verified atpackages/api/src/lib/read-only-prisma.ts:26-32):$executeRaw$executeRawUnsafe$transaction$queryRawUnsafe(template-tagged$queryRawintentionally allowed — read-only by API contract)$runCommandRawPart 2 — Scoped-caller forwarding (commit
1ff5c33, verified atpackages/api/src/router/assistant-tools/helpers.ts:1670-1687andpackages/api/src/router/assistant-tools.ts:740):executeToolwrapsctx.dbwithcreateReadOnlyProxy(ctx.db)for every non-mutation tool BEFORE the executor runs.createScopedCallerContextforwardsctx.dbto the tRPC caller verbatim — sincectx.dbis already the proxy, the caller inherits it transparently.packages/api/src/__tests__/read-only-scoped-caller.test.ts(3 tests, commitb9040cb) proves writes, raw SQL, and$transactionall throw through the forwarded db.Part 3 — Early access check (verified at
packages/api/src/router/assistant-tools.ts:728-736andpackages/api/src/router/assistant-tools/access-control.ts):getAssistantToolAccessFailure(toolDefinition, ctx)runs at the top ofexecuteTool, BEFOREJSON.parse(args)and BEFORE the executor is invoked. No DB lookup happens before auth fires.advanced-timeline.ts:617already hasassertPermissionas the first two lines ofquick_assign_timeline_resource(before the DB resolver calls at line 620).assertPermissioncalls remain as defense-in-depth.Part 4 — Regression test suite:
packages/api/src/__tests__/read-only-prisma.test.ts(7 tests) — covers the proxy itselfpackages/api/src/__tests__/read-only-scoped-caller.test.ts(3 tests) — covers scoped-caller forwardingAll acceptance criteria met — closing.