fix(SEC-API-27): Scope RLS context to transaction boundary
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

createAuthMiddleware was calling SET LOCAL on the raw PrismaClient
outside of any transaction. In PostgreSQL, SET LOCAL without a
transaction acts as a session-level SET, which can leak RLS context
to subsequent requests sharing the same pooled connection, enabling
cross-tenant data access.

Wrapped the setCurrentUser call and downstream handler execution
inside a $transaction block so SET LOCAL is automatically reverted
when the transaction ends (on both success and failure).

Added comprehensive test suite for db-context module verifying:
- RLS context is set on the transaction client, not the raw client
- next() executes inside the transaction boundary
- Authentication errors prevent any transaction from starting
- Errors in downstream handlers propagate correctly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-06 15:07:49 -06:00
parent 617df12b52
commit 2e11931ded
2 changed files with 239 additions and 3 deletions

View File

@@ -349,12 +349,18 @@ export function createAuthMiddleware(client: PrismaClient) {
ctx: { userId?: string };
next: () => Promise<unknown>;
}): Promise<unknown> {
if (!opts.ctx.userId) {
const { userId } = opts.ctx;
if (!userId) {
throw new Error("User not authenticated");
}
await setCurrentUser(opts.ctx.userId, client);
return opts.next();
// SEC-API-27: SET LOCAL must be called inside a transaction boundary.
// Without a transaction, SET LOCAL behaves as a session-level SET,
// which can leak RLS context to other requests via connection pooling.
return client.$transaction(async (tx) => {
await setCurrentUser(userId, tx as PrismaClient);
return opts.next();
});
};
}