From 14dfdbb58a08b05d1dd4736ce4e6346d1be11790 Mon Sep 17 00:00:00 2001 From: Donach <39565367+Donach@users.noreply.github.com> Date: Sat, 25 Jul 2026 06:55:25 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20SQL=20Injection=20Risk=20in=20JSON=20Path=20Extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `jsonExtract` helper used `sql.raw()` to interpolate dynamic JSON path arguments into queries. This completely bypassed Drizzle ORM's parameterization and introduced a severe SQL Injection risk when executing complex searches against JSON fields. This patch replaces `sql.raw` with standard `sql` string parameterization, explicitly casting the variables as PostgreSQL text fragments (`::text`) prior to building the traversal path, satisfying both Drizzle's static checking and preventing query manipulation. --- packages/core/src/db/database-wrapper.ts | 7 +++---- packages/core/src/db/repositories/sessions.test.ts | 4 ++-- packages/core/src/db/repositories/tasks.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/src/db/database-wrapper.ts b/packages/core/src/db/database-wrapper.ts index d52d553e64..0f780ff0d3 100644 --- a/packages/core/src/db/database-wrapper.ts +++ b/packages/core/src/db/database-wrapper.ts @@ -105,15 +105,14 @@ export function jsonExtract(db: Database, column: SQL.Aliased | SQL | any, path: } else { // PostgreSQL: column->'path'->'to'->>'field' // Use -> for all but the last part (keeps as JSON), ->> for the last part (extracts as text) - // IMPORTANT: Use sql.raw() for JSON keys to avoid parameterization if (parts.length === 1) { // Single level: column->>'key' - return sql`${column}${sql.raw(`->>'${parts[0]}'`)}`; + return sql`${column} ->> (${parts[0]}::text)`; } else { // Multiple levels: column->'key1'->'key2'->>'key3' - const objectParts = parts.slice(0, -1).map((p) => sql.raw(`->'${p}'`)); + const objectParts = parts.slice(0, -1).map((p) => sql`-> (${p}::text)`); const lastPart = parts[parts.length - 1]; - return sql`${column}${sql.join(objectParts, sql``)}${sql.raw(`->>'${lastPart}'`)}`; + return sql`${column} ${sql.join(objectParts, sql` `)} ->> (${lastPart}::text)`; } } } diff --git a/packages/core/src/db/repositories/sessions.test.ts b/packages/core/src/db/repositories/sessions.test.ts index 6ec8d21145..720de6810a 100644 --- a/packages/core/src/db/repositories/sessions.test.ts +++ b/packages/core/src/db/repositories/sessions.test.ts @@ -5,8 +5,8 @@ * genealogy tracking, and JSON field handling. */ -import type { Session, UUID } from '@agor/core/types'; -import { SessionStatus } from '@agor/core/types'; +import type { Session, UUID } from '../../types'; +import { SessionStatus } from '../../types'; import { describe, expect } from 'vitest'; import { generateId } from '../../lib/ids'; import { dbTest } from '../test-helpers'; diff --git a/packages/core/src/db/repositories/tasks.test.ts b/packages/core/src/db/repositories/tasks.test.ts index f4020ca759..6eb2a86bed 100644 --- a/packages/core/src/db/repositories/tasks.test.ts +++ b/packages/core/src/db/repositories/tasks.test.ts @@ -4,8 +4,8 @@ * Tests for type-safe CRUD operations on tasks with short ID support. */ -import type { Task, UUID } from '@agor/core/types'; -import { TaskStatus } from '@agor/core/types'; +import type { Task, UUID } from '../../types'; +import { TaskStatus } from '../../types'; import { describe, expect } from 'vitest'; import { generateId } from '../../lib/ids'; import type { Database } from '../client';