Skip to content

Query Safety And Types

R-SQL-1 - Untrusted values use prepared statement binding

Rule. Request data, auth claims, route params, form values, and tenant IDs enter SQL through D1 prepared statement parameters such as ? plus .bind(...), never string interpolation.

Why. Cloudflare recommends D1 prepared statements with bound parameters, and OWASP identifies parameterized queries as the primary SQL injection defense.

vcqa. Flag template literals, string concatenation, or .replace()-built SQL that includes untrusted values; allow static SQL strings with .bind(...) for values.

References. https://developers.cloudflare.com/d1/worker-api/prepared-statements/, https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html

R-SQL-2 - Dynamic identifiers are allow-listed

Rule. Dynamic table names, column names, sort fields, directions, and JSON paths are selected from server-side allow-lists, not bound as values or interpolated from requests.

Why. Prepared statement parameters protect values, not SQL structure. Identifier selection needs an explicit allow-list.

vcqa. Flag user-controlled values inside SQL identifiers or ORDER BY/LIMIT/table position unless guarded by enum/schema validation and a fixed mapping.

References. https://developers.cloudflare.com/d1/worker-api/prepared-statements/, https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html

R-SQL-3 - Raw exec() is restricted to trusted SQL

Rule. D1Database.exec() is used only for trusted SQL files, migrations, diagnostics, or internal setup; application request paths use prepared statements.

Why. exec() runs SQL text directly. In request handling, prepared statements provide the safer and typed D1 API surface.

vcqa. Flag env.DB.exec(...) reachable from request data or accepting variable SQL outside migration/test utilities.

References. https://developers.cloudflare.com/d1/worker-api/d1-database/, https://developers.cloudflare.com/d1/worker-api/prepared-statements/

R-TYPE-1 - D1 rows are typed and validated at boundaries

Rule. Query result rows have TypeScript types or schemas, and data crossing request, auth, or external API boundaries is runtime-validated before it influences SQL.

Why. D1's TypeScript API can type returned rows, but database and request boundaries are runtime data. Static casts alone do not prove shape or authorization.

vcqa. Check for run<T>(), first<T>(), row interfaces, schema parsers, and boundary validation. Flag blind as T casts on request JSON or database rows used in critical logic.

References. https://developers.cloudflare.com/d1/worker-api/, https://developers.cloudflare.com/d1/worker-api/return-object/

R-SQL-4 - Tenant and lookup predicates are indexed

Rule. High-cardinality tenant keys, foreign keys, and frequent lookup predicates used by request paths have indexes created by migrations.

Why. D1 uses SQLite semantics. Tenant-scoped apps commonly filter by tenant and object IDs on every request; missing indexes can turn authorization-safe queries into latency and cost problems.

vcqa. Compare query predicates with migration-created indexes; flag tenant-owned tables with frequent WHERE tenant_id = ? or join predicates and no supporting index.

References. https://developers.cloudflare.com/d1/best-practices/use-indexes/

R-SQL-5 - Error responses do not leak SQL internals

Rule. Database errors returned to clients are safe, generic messages; detailed SQL, bindings, tenant IDs, and stack traces stay in server-side logs with redaction.

Why. D1-backed routes often expose user-facing APIs. Raw SQL errors can reveal schema and tenant boundaries while making injection probing easier.

vcqa. Flag response bodies containing SQL text, stack traces, migration table names, or raw D1 errors from catch blocks.

References. https://developers.cloudflare.com/d1/worker-api/return-object/, https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html