From a8124fa6b95ad97e1f8f11bfb692820ad7d2e66e Mon Sep 17 00:00:00 2001 From: "CTO (LegalAI)" Date: Thu, 9 Apr 2026 09:05:02 +0000 Subject: [PATCH] fix: use set_config() instead of SET LOCAL for tenant RLS context PostgreSQL SET commands do not support parameterized queries ($1), causing "syntax error at or near $1" on all tenant-scoped operations. Replaced with set_config('app.tenant_id', $1, true) which supports parameters safely. Also added BEGIN/COMMIT transaction wrapping since set_config(..., true) requires a transaction for LOCAL scope. Fixed SQL injection vulnerability in tenant.ts which used unescaped string interpolation. Co-Authored-By: Paperclip --- src/lib/db/index.ts | 15 ++++++++++----- src/lib/db/tenant.ts | 10 ++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index e876ca9..c1baad9 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -28,13 +28,18 @@ export async function withTenantDb( ): Promise { const client: PoolClient = await pool.connect(); try { - // Set tenant context for RLS — uses parameterized SET to prevent SQL injection - await client.query(`SET LOCAL app.tenant_id = $1`, [tenantId]); + await client.query('BEGIN'); + // Set tenant context for RLS — set_config supports parameterized queries + // Third arg `true` = LOCAL (scoped to current transaction only) + await client.query(`SELECT set_config('app.tenant_id', $1, true)`, [tenantId]); const tenantDb = drizzle(client, { schema }); - return await callback(tenantDb); + const result = await callback(tenantDb); + await client.query('COMMIT'); + return result; + } catch (error) { + await client.query('ROLLBACK'); + throw error; } finally { - // RESET ensures no tenant context leaks to the next user of this connection - await client.query(`RESET app.tenant_id`); client.release(); } } diff --git a/src/lib/db/tenant.ts b/src/lib/db/tenant.ts index 0cee838..eb719f7 100644 --- a/src/lib/db/tenant.ts +++ b/src/lib/db/tenant.ts @@ -14,8 +14,14 @@ export async function withTenant( ): Promise { const client = await pool.connect(); try { - await client.query(`SET LOCAL app.tenant_id = '${tenantId}'`); - return await fn(client); + await client.query('BEGIN'); + await client.query(`SELECT set_config('app.tenant_id', $1, true)`, [tenantId]); + const result = await fn(client); + await client.query('COMMIT'); + return result; + } catch (error) { + await client.query('ROLLBACK'); + throw error; } finally { client.release(); }