Files
promptfoo--promptfoo/docs/agents/database-security.md
T
wehub-resource-sync 0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
CI / Shell Format Check (push) Waiting to run
CI / Check Ruby (3.4) (push) Waiting to run
CI / CI Config (push) Waiting to run
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Blocked by required conditions
CI / Build on Node ${{ matrix.node }} (push) Blocked by required conditions
CI / Style Check (push) Waiting to run
CI / Generate Assets (push) Waiting to run
CI / Check Python (3.14) (push) Waiting to run
CI / Check Python (3.9) (push) Waiting to run
CI / Build Docs (push) Waiting to run
CI / Code Scan Action (push) Waiting to run
CI / Site tests (push) Waiting to run
CI / webui tests (push) Waiting to run
CI / Run Integration Tests (push) Waiting to run
CI / Run Smoke Tests (push) Waiting to run
CI / Go Tests (push) Waiting to run
CI / Share Test (push) Waiting to run
CI / Redteam (Production API) (push) Waiting to run
CI / Redteam (Staging API) (push) Waiting to run
CI / GitHub Actions Lint (push) Waiting to run
CI / Check Ruby (3.0) (push) Waiting to run
release-please / release-please (push) Waiting to run
release-please / build (push) Blocked by required conditions
release-please / publish-npm (push) Blocked by required conditions
release-please / publish-npm-backfill (push) Waiting to run
release-please / docker (push) Blocked by required conditions
release-please / publish-code-scan-action (push) Blocked by required conditions
release-please / attest-code-scan-action (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

3.4 KiB

Database Security (SQL Injection Prevention)

This codebase uses Drizzle ORM with SQLite. All database queries must use parameterized SQL so user-controlled input never changes query structure.

Quick Rules

  • Use Drizzle's sql tagged template literals for dynamic values.
  • Use sql.join() for dynamic lists such as IN (...) clauses.
  • Pass SQL<unknown> fragments between functions, not strings.
  • Do not build queries with sql.raw() or string interpolation.
  • Prefer json_each() for user-selected JSON keys. When json_extract() is appropriate, bind a path built with the vetted buildSafeJsonPath helper in src/models/eval.ts.

Required Pattern: Use sql Template Strings

Use parameterized queries for single values:

import { sql } from 'drizzle-orm';

const query = sql`SELECT * FROM eval_results WHERE eval_id = ${evalId}`;

Use parameterized queries for multiple values:

import { sql } from 'drizzle-orm';

const query = sql`
  SELECT * FROM eval_results
  WHERE eval_id = ${evalId} AND success = ${1}
`;

Use sql.join() for dynamic IN (...) lists:

import { sql } from 'drizzle-orm';

const ids = ['id1', 'id2', 'id3'];
const query = sql`SELECT * FROM evals WHERE id IN (${sql.join(ids, sql`, `)})`;

Forbidden Pattern: Raw SQL with Dynamic Content

Do not interpolate user-controlled values into raw SQL:

const query = sql.raw(`SELECT * FROM eval_results WHERE eval_id = '${evalId}'`);

Do not build SQL conditions with string concatenation:

import { sql } from 'drizzle-orm';

const whereClause = `eval_id = '${evalId}'`;
const query = sql.raw(`SELECT * FROM eval_results WHERE ${whereClause}`);

JSON Paths in SQLite

SQLite's json_extract() accepts its JSON path as a bound value. Do not use sql.raw() to splice user-controlled JSON paths into SQL. Prefer json_each() when filtering by dynamic keys, because the key can be compared as a normal parameter:

import { sql } from 'drizzle-orm';

const query = sql`
  SELECT *
  FROM eval_results
  WHERE EXISTS (
    SELECT 1
    FROM json_each(metadata)
    WHERE json_each.key = ${field} AND json_each.value = ${value}
  )
`;

If you construct a JSON path for json_extract(), use buildSafeJsonPath from src/models/eval.ts, which escapes backslashes and double quotes for JSON path syntax and returns a value to bind:

import { sql } from 'drizzle-orm';
import { buildSafeJsonPath } from '../../src/models/eval';

const jsonPath = buildSafeJsonPath(userField);
const query = sql`
  SELECT * FROM eval_results
  WHERE json_extract(metadata, ${jsonPath}) = ${value}
`;

If you need a new JSON-path helper, match the guarantees in buildSafeJsonPath and keep the implementation audited in one shared utility.

Passing SQL Fragments Between Functions

When building complex queries, pass SQL<unknown> fragments instead of strings:

import { type SQL, sql } from 'drizzle-orm';

function queryWithFilter(whereSql: SQL<unknown>): Promise<Result[]> {
  const query = sql`SELECT * FROM eval_results WHERE ${whereSql}`;
  return db.all(query);
}

const filter = sql`eval_id = ${evalId} AND success = ${1}`;
const results = await queryWithFilter(filter);

Key Files with Database Queries

  • src/models/eval.ts - Main eval queries and JSON-path helper
  • src/util/calculateFilteredMetrics.ts - Metrics aggregation queries
  • src/database/index.ts - Database connection