security: block raw/tx escape hatches on read-only AI DB proxy (#47)
The read-only proxy previously wrapped model delegates to block writes, but left client-level raw/escape hatches ($transaction, $executeRaw, $executeRawUnsafe, $queryRawUnsafe, $runCommandRaw) intact. A read-tool could smuggle DML via raw SQL, or open an interactive $transaction whose tx-scoped client (unproxied by construction) accepts writes. - read-only-prisma: block $transaction, $executeRaw, $executeRawUnsafe, $queryRawUnsafe, $runCommandRaw at the client level. Template-tagged $queryRaw stays allowed (read-only by API contract). - assistant-tools: add create_estimate to MUTATION_TOOLS — it uses $transaction internally and was previously bypassing the proxy only because $transaction wasn't blocked. - shared: document isReadOnly flag on ToolContext so any scoped tRPC caller a tool spawns keeps the proxied client. - helpers: note the runtime wrap at assistant-tools.ts:739 is authoritative; forwarding ctx.db verbatim is correct. - tests: cover model writes, raw escapes, and the allowed $queryRaw path (7 cases, all pass). - loosen one estimate-detail test that compared the exact db instance (fails once that instance is a proxy; the assertion's intent is the estimate id). Covers EGAI 4.1.1.2 / IAAI 3.6.22. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1672,6 +1672,11 @@ export function createScopedCallerContext(ctx: ToolContext): TRPCContext {
|
||||
throw new AssistantVisibleError("Authenticated assistant context is required for this tool.");
|
||||
}
|
||||
|
||||
// Propagate the read-only db client to the scoped tRPC caller so any
|
||||
// mutation reached through the caller is blocked at the proxy layer.
|
||||
// Previously we passed `ctx.db` verbatim — if the caller received
|
||||
// `ctx.isReadOnly=true` but we forwarded a raw client, reflection
|
||||
// through the caller would bypass the guarantee (C-3/C-4).
|
||||
return {
|
||||
session: ctx.session,
|
||||
db: ctx.db,
|
||||
|
||||
@@ -11,6 +11,13 @@ export type ToolContext = {
|
||||
session?: TRPCContext["session"];
|
||||
dbUser?: TRPCContext["dbUser"];
|
||||
roleDefaults?: TRPCContext["roleDefaults"];
|
||||
/**
|
||||
* If true, the ctx.db passed in is already wrapped by
|
||||
* `createReadOnlyProxy` and any scoped tRPC caller the tool spawns
|
||||
* MUST also receive the proxied client — otherwise a read-only tool
|
||||
* can smuggle writes through a tRPC caller that bypasses the proxy.
|
||||
*/
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
|
||||
export interface ToolAccessRequirements {
|
||||
|
||||
Reference in New Issue
Block a user