# How to Ensure Safe SQL Patterns in Cursor Output

- Tool: Cursor
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Cursor Free+, any SQL database project
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
---
description: Enforce parameterized queries for PostgreSQL
globs: "*.ts,*.js,*.sql,*.repository.ts,*.service.ts"
alwaysApply: true
---

# SQL Parameterization Rules (PostgreSQL)
- ALWAYS use $1, $2, $3 numbered placeholders in queries
- NEVER use template literals with ${} in SQL strings
- NEVER concatenate user input into SQL with + operator
- NEVER use string interpolation in WHERE, INSERT VALUES, or UPDATE SET
- For dynamic IN clauses: generate $1, $2, ... $N based on array length
- For dynamic WHERE: build conditions array and join with AND
- For LIKE/ILIKE: put wildcards in the parameter value, not the query

## Safe Pattern:
```typescript
await db.query('SELECT * FROM users WHERE email = $1 AND active = $2', [email, true]);
```

## Unsafe Pattern (NEVER):
```typescript
await db.query(`SELECT * FROM users WHERE email = '${email}'`);
```
```

**Expected result:** Cursor generates PostgreSQL queries with $1/$2 numbered placeholders instead of string interpolation.

### 2. 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.

```
interface QueryBuilder {
  text: string;
  params: (string | number | boolean | null)[];
}

export const buildWhereClause = (
  filters: Record<string, unknown>
): QueryBuilder => {
  const conditions: string[] = [];
  const params: (string | number | boolean | null)[] = [];
  let paramIndex = 1;

  for (const [key, value] of Object.entries(filters)) {
    if (value === undefined) continue;

    if (typeof value === 'string' && key.endsWith('Like')) {
      const column = key.replace('Like', '');
      conditions.push(`${column} ILIKE $${paramIndex++}`);
      params.push(`%${value}%`);
    } else if (Array.isArray(value)) {
      const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
      conditions.push(`${key} IN (${placeholders})`);
      params.push(...value);
    } else {
      conditions.push(`${key} = $${paramIndex++}`);
      params.push(value as string | number | boolean | null);
    }
  }

  return {
    text: conditions.length ? `WHERE ${conditions.join(' AND ')}` : '',
    params,
  };
};
```

**Expected result:** A safe query builder that Cursor imports for all dynamic WHERE clause generation.

### 3. 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.

```
@sql-params.mdc @src/db/query-builder.ts

Create an OrderRepository with these methods:
1. findAll(filters: OrderFilters) — dynamic WHERE with buildWhereClause
2. findById(id: string) — parameterized $1
3. findByDateRange(start: Date, end: Date) — parameterized BETWEEN
4. create(data: CreateOrderDTO) — parameterized INSERT with RETURNING
5. bulkUpdateStatus(ids: string[], status: string) — parameterized IN clause

All queries MUST use parameterized placeholders.
Use buildWhereClause for dynamic filtering.
Never 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.

### 4. 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.

```
@sql-params.mdc @codebase

Search the entire codebase for SQL injection vulnerabilities:
1. Template literals containing SQL keywords with ${} interpolation
2. String concatenation with + near SQL query strings
3. Raw query calls without parameterized placeholders
4. LIKE clauses with interpolated wildcards
5. IN clauses built with string joining

For each vulnerability:
- Show the file and line number
- Explain the injection risk
- Show the parameterized fix
```

**Expected result:** Cursor identifies all SQL injection risks in the codebase with specific parameterized fixes.

### 5. 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.

```
@sql-params.mdc @src/repositories/order.repository.ts

Generate security tests for OrderRepository that verify SQL injection
prevention. For each method, test with these injection payloads:
1. "'; DROP TABLE orders; --"
2. "1' OR '1'='1"
3. "1; UPDATE users SET role='admin'"
4. "' UNION SELECT * FROM users --"

Each test should verify:
- The query executes without error
- No unintended data is returned or modified
- The injection payload is treated as a literal string value
```

**Expected result:** Cursor generates injection test cases that verify parameterized queries treat all input as literal values.

## Complete code example

File: `src/db/query-builder.ts`

```typescript
interface QueryBuilder {
  text: string;
  params: (string | number | boolean | null)[];
}

export const buildWhereClause = (
  filters: Record<string, unknown>
): QueryBuilder => {
  const conditions: string[] = [];
  const params: (string | number | boolean | null)[] = [];
  let paramIndex = 1;

  for (const [key, value] of Object.entries(filters)) {
    if (value === undefined) continue;

    if (typeof value === 'string' && key.endsWith('Like')) {
      const column = key.replace('Like', '');
      conditions.push(`${column} ILIKE $${paramIndex++}`);
      params.push(`%${value}%`);
    } else if (Array.isArray(value)) {
      const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
      conditions.push(`${key} IN (${placeholders})`);
      params.push(...(value as (string | number | boolean | null)[]));
    } else if (value === null) {
      conditions.push(`${key} IS NULL`);
    } else {
      conditions.push(`${key} = $${paramIndex++}`);
      params.push(value as string | number | boolean);
    }
  }

  return {
    text: conditions.length ? `WHERE ${conditions.join(' AND ')}` : '',
    params,
  };
};

export const buildInsert = (
  table: string,
  data: Record<string, unknown>
): QueryBuilder => {
  const keys = Object.keys(data);
  const values = Object.values(data) as (string | number | boolean | null)[];
  const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');

  return {
    text: `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders}) RETURNING *`,
    params: values,
  };
};
```

## Common mistakes

- **Parameterizing simple queries but not dynamic WHERE clauses** — Cursor often falls back to string interpolation for complex dynamic filters because parameterized dynamic queries require more code. This is where most SQL injection occurs. Fix: Create a buildWhereClause utility and reference it in your rules. Cursor uses the utility for dynamic queries instead of string interpolation.
- **Putting wildcards in the SQL string instead of the parameter** — Cursor generates LIKE '%${search}%' instead of LIKE $1 with the parameter set to '%search%'. The wildcard must be in the parameter value, not the query. Fix: Show the correct LIKE pattern in your rules: ILIKE $1 with params.push('%' + search + '%').
- **Not testing with actual injection payloads** — Code review alone cannot guarantee parameterization works correctly. Subtle bugs in dynamic query building can still allow injection. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/cursor-tutorial/how-to-ensure-cursor-ai-uses-parameterized-queries-in-sql-for-preventing-injections
© RapidDev — https://www.rapidevelopers.com/cursor-tutorial/how-to-ensure-cursor-ai-uses-parameterized-queries-in-sql-for-preventing-injections
