Cursor-generated SQL code often uses string interpolation or concatenation to build queries, creating SQL injection vulnerabilities. By creating .cursor/rules/ that mandate parameterized placeholders, providing a typed query wrapper for Cursor to import, and reinforcing safety in every prompt, you ensure all generated database queries use parameter binding regardless of the query complexity or database engine.
Ensuring safe SQL patterns in Cursor output
Parameterized queries are the foundation of SQL injection prevention. While ORM methods are generally safe, Cursor often generates raw queries with string interpolation for complex scenarios like dynamic filters, bulk operations, and reporting queries. This tutorial establishes comprehensive rules and utilities that ensure every SQL query Cursor generates uses parameter binding.
Prerequisites
- Cursor installed with a project that uses SQL
- Basic understanding of SQL injection and parameterized queries
- A database driver installed (pg, mysql2, better-sqlite3, etc.)
- Familiarity with Cursor project rules and Chat
Step-by-step guide
Create database-specific parameterization rules
Create database-specific parameterization rules
Different databases use different placeholder syntax. Create rules that match your specific database engine so Cursor generates the correct parameterized syntax.
1---2description: Enforce parameterized queries for PostgreSQL3globs: "*.ts,*.js,*.sql,*.repository.ts,*.service.ts"4alwaysApply: true5---67# SQL Parameterization Rules (PostgreSQL)8- ALWAYS use $1, $2, $3 numbered placeholders in queries9- NEVER use template literals with ${} in SQL strings10- NEVER concatenate user input into SQL with + operator11- NEVER use string interpolation in WHERE, INSERT VALUES, or UPDATE SET12- For dynamic IN clauses: generate $1, $2, ... $N based on array length13- For dynamic WHERE: build conditions array and join with AND14- For LIKE/ILIKE: put wildcards in the parameter value, not the query1516## Safe Pattern:17```typescript18await db.query('SELECT * FROM users WHERE email = $1 AND active = $2', [email, true]);19```2021## Unsafe Pattern (NEVER):22```typescript23await db.query(`SELECT * FROM users WHERE email = '${email}'`);24```Expected result: Cursor generates PostgreSQL queries with $1/$2 numbered placeholders instead of string interpolation.
Build a safe dynamic query builder
Build a safe dynamic query builder
Dynamic WHERE clauses are the most common source of SQL injection in Cursor output. Create a utility that builds parameterized dynamic queries safely, then reference it in all database prompts.
1interface QueryBuilder {2 text: string;3 params: (string | number | boolean | null)[];4}56export const buildWhereClause = (7 filters: Record<string, unknown>8): QueryBuilder => {9 const conditions: string[] = [];10 const params: (string | number | boolean | null)[] = [];11 let paramIndex = 1;1213 for (const [key, value] of Object.entries(filters)) {14 if (value === undefined) continue;1516 if (typeof value === 'string' && key.endsWith('Like')) {17 const column = key.replace('Like', '');18 conditions.push(`${column} ILIKE $${paramIndex++}`);19 params.push(`%${value}%`);20 } else if (Array.isArray(value)) {21 const placeholders = value.map(() => `$${paramIndex++}`).join(', ');22 conditions.push(`${key} IN (${placeholders})`);23 params.push(...value);24 } else {25 conditions.push(`${key} = $${paramIndex++}`);26 params.push(value as string | number | boolean | null);27 }28 }2930 return {31 text: conditions.length ? `WHERE ${conditions.join(' AND ')}` : '',32 params,33 };34};Expected result: A safe query builder that Cursor imports for all dynamic WHERE clause generation.
Generate repository code using the safe patterns
Generate repository code using the safe patterns
Prompt Cursor to create database access code that uses your parameterized query patterns. Reference both the rules and the query builder utility so Cursor uses them consistently.
1@sql-params.mdc @src/db/query-builder.ts23Create an OrderRepository with these methods:41. findAll(filters: OrderFilters) — dynamic WHERE with buildWhereClause52. findById(id: string) — parameterized $163. findByDateRange(start: Date, end: Date) — parameterized BETWEEN74. create(data: CreateOrderDTO) — parameterized INSERT with RETURNING85. bulkUpdateStatus(ids: string[], status: string) — parameterized IN clause910All queries MUST use parameterized placeholders.11Use buildWhereClause for dynamic filtering.12Never use string interpolation in any SQL.Expected result: Cursor generates a repository with parameterized queries for every method, using the query builder for dynamic filtering.
Audit existing code for unsafe SQL patterns
Audit existing code for unsafe SQL patterns
Use Cursor to scan your entire codebase for SQL injection vulnerabilities. The @codebase context lets Cursor search across all files for dangerous patterns like template literals near SQL keywords.
1@sql-params.mdc @codebase23Search the entire codebase for SQL injection vulnerabilities:41. Template literals containing SQL keywords with ${} interpolation52. String concatenation with + near SQL query strings63. Raw query calls without parameterized placeholders74. LIKE clauses with interpolated wildcards85. IN clauses built with string joining910For each vulnerability:11- Show the file and line number12- Explain the injection risk13- Show the parameterized fixExpected result: Cursor identifies all SQL injection risks in the codebase with specific parameterized fixes.
Test parameterized queries with injection attempts
Test parameterized queries with injection attempts
Generate tests that verify your queries are actually parameterized by attempting SQL injection payloads. These tests confirm that the parameterized approach prevents injection regardless of input.
1@sql-params.mdc @src/repositories/order.repository.ts23Generate security tests for OrderRepository that verify SQL injection4prevention. For each method, test with these injection payloads:51. "'; DROP TABLE orders; --"62. "1' OR '1'='1"73. "1; UPDATE users SET role='admin'"84. "' UNION SELECT * FROM users --"910Each test should verify:11- The query executes without error12- No unintended data is returned or modified13- The injection payload is treated as a literal string valueExpected result: Cursor generates injection test cases that verify parameterized queries treat all input as literal values.
Complete working example
1interface QueryBuilder {2 text: string;3 params: (string | number | boolean | null)[];4}56export const buildWhereClause = (7 filters: Record<string, unknown>8): QueryBuilder => {9 const conditions: string[] = [];10 const params: (string | number | boolean | null)[] = [];11 let paramIndex = 1;1213 for (const [key, value] of Object.entries(filters)) {14 if (value === undefined) continue;1516 if (typeof value === 'string' && key.endsWith('Like')) {17 const column = key.replace('Like', '');18 conditions.push(`${column} ILIKE $${paramIndex++}`);19 params.push(`%${value}%`);20 } else if (Array.isArray(value)) {21 const placeholders = value.map(() => `$${paramIndex++}`).join(', ');22 conditions.push(`${key} IN (${placeholders})`);23 params.push(...(value as (string | number | boolean | null)[]));24 } else if (value === null) {25 conditions.push(`${key} IS NULL`);26 } else {27 conditions.push(`${key} = $${paramIndex++}`);28 params.push(value as string | number | boolean);29 }30 }3132 return {33 text: conditions.length ? `WHERE ${conditions.join(' AND ')}` : '',34 params,35 };36};3738export const buildInsert = (39 table: string,40 data: Record<string, unknown>41): QueryBuilder => {42 const keys = Object.keys(data);43 const values = Object.values(data) as (string | number | boolean | null)[];44 const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');4546 return {47 text: `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders}) RETURNING *`,48 params: values,49 };50};Common mistakes when ensuring Safe SQL Patterns in Cursor Output
Why it's a problem: Parameterizing simple queries but not dynamic WHERE clauses
How to avoid: Create a buildWhereClause utility and reference it in your rules. Cursor uses the utility for dynamic queries instead of string interpolation.
Why it's a problem: Putting wildcards in the SQL string instead of the parameter
How to avoid: Show the correct LIKE pattern in your rules: ILIKE $1 with params.push('%' + search + '%').
Why it's a problem: Not testing with actual injection payloads
How to avoid: Generate and run security tests with common injection payloads to verify parameterization prevents them.
Best practices
- Create a typed query builder utility for dynamic WHERE clauses
- Reference package.json and database driver in prompts for correct placeholder syntax
- Test with SQL injection payloads after every database code change
- Use @codebase audits periodically to catch unsafe patterns
- Keep raw SQL queries in dedicated repository files for easier review
- Use ORMs for simple CRUD and parameterized raw queries for complex operations
- Never trust that Cursor's output is safe — always review SQL code manually
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Create a safe dynamic query builder for PostgreSQL that generates parameterized WHERE clauses from a filter object. Support equality, ILIKE, IN, NULL, date ranges, and pagination. All user input must go through $1/$2 parameters.
@sql-params.mdc @src/db/query-builder.ts Generate a ReportRepository with complex queries: monthly revenue aggregation, top products by sales, customer retention rates. All queries MUST use parameterized $1/$2 placeholders. Use buildWhereClause for dynamic date range filtering.
Frequently asked questions
Are ORMs like Prisma or TypeORM always safe from injection?
Standard ORM methods are safe, but raw query methods (Prisma.$queryRaw, TypeORM query builder with .where(string)) can be vulnerable. Add ORM-specific rules for raw query usage.
What about NoSQL injection in MongoDB?
MongoDB has its own injection risks through operator injection. Create separate rules for MongoDB that ban $where and mandate sanitized query objects.
How do I parameterize ORDER BY clauses?
Most databases do not support parameterized ORDER BY. Validate the column name against a whitelist of allowed columns, then interpolate it safely (not from user input directly).
Should I use prepared statements or parameterized queries?
Both are safe. Prepared statements offer a small performance benefit for repeated queries. Parameterized queries are simpler for one-off queries. Both prevent SQL injection.
Can Cursor detect SQL injection in existing code?
Yes. Use the @codebase audit prompt to scan for template literals near SQL keywords. Cursor identifies most common injection patterns but may miss subtle edge cases.
Can RapidDev help secure our database layer?
Yes. RapidDev provides database security audits, builds parameterized query utilities, and configures Cursor rules and CI scanning for ongoing SQL injection prevention.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation