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 <noreply@paperclip.ing>
This commit is contained in:
CTO (LegalAI)
2026-04-09 09:05:02 +00:00
parent 517184bbae
commit a8124fa6b9
2 changed files with 18 additions and 7 deletions

View File

@@ -28,13 +28,18 @@ export async function withTenantDb<T>(
): Promise<T> {
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();
}
}

View File

@@ -14,8 +14,14 @@ export async function withTenant<T>(
): Promise<T> {
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();
}