DEV Community

BusyAgents
BusyAgents

Posted on

The SQL Injection That Lives in Every AI-Generated Search Endpoint

TL;DR

  • AI copilots generate SQL queries using template literals - that is CWE-89 SQL injection

  • Your database cannot tell the difference between a legit request and an attack

  • Fix is one line: parameterized queries. Your driver handles the rest.

A client sent me their Node.js API to review three weeks ago. Express, PostgreSQL, Prisma for most of the data layer. They had built the whole thing in Cursor over about two weeks. Code quality was genuinely good - TypeScript throughout, consistent patterns, proper error handling. The AI had done a solid job structuring it.

Then I hit the order search endpoint.

The Vulnerable Code (CWE-89)

The developer needed a flexible search with multiple optional filters. The AI generated this:

app.get('/api/orders/search', async (req, res) => {
  const { customer, status, dateFrom } = req.query;
  const results = await db.query(
    `SELECT * FROM orders 
     WHERE customer_name = '${customer}'
     AND status = '${status}'
     AND created_at >= '${dateFrom}'`
  );
  res.json(results.rows);
});
Enter fullscreen mode Exit fullscreen mode

Send ' OR '1'='1' -- as the customer parameter and you get every order in the database. No authentication required. Send '; DELETE FROM orders WHERE '1'='1 and the table is gone.

I showed the developer. They were surprised. They had asked the AI to "simplify the query" mid-session and it swapped out the parameterized version it had written first. One refactoring prompt, protection gone.

Why AI Keeps Writing This

This is not a knowledge gap. Ask the same LLM "is this query vulnerable to SQL injection?" and it will explain the problem accurately, cite CWE-89, and suggest the fix.

The issue is generation context. When a model completes a "search with filters" function, it pattern-matches from training data. And training data has an enormous amount of quick-start tutorials, Stack Overflow answers, and legacy documentation showing string interpolation in database queries. The "gets it working" pattern outnumbers the production-safe version by a lot.

Models do not ship software. They have no stake in what happens to your database six months from now.

This pattern shows up in every language. JavaScript with template literals. Python with f-strings. Go with fmt.Sprintf in query strings. PHP with string concatenation. Same root cause, same fix.

The Fix

Parameterized queries. The query string is static. User input goes in a separate array. Your database driver handles escaping at the driver level - the database never sees user input as SQL syntax.

Node.js with node-postgres:

// Vulnerable - do not ship this
const results = await db.query(
  `SELECT * FROM orders WHERE customer_name = '${customer}' AND status = '${status}'`
);

// Safe - parameterized
const results = await db.query(
  'SELECT * FROM orders WHERE customer_name = $1 AND status = $2',
  [customer, status]
);
Enter fullscreen mode Exit fullscreen mode

Python with psycopg2:

# Vulnerable
cursor.execute(f"SELECT * FROM users WHERE email = '{email}' AND role = '{role}'")

# Safe
cursor.execute(
    "SELECT * FROM users WHERE email = %s AND role = %s",
    (email, role)
)
Enter fullscreen mode Exit fullscreen mode

The rule: if the string that gets passed to db.query() ever changes based on user input, something is wrong.

Most modern ORMs are safe by default - Prisma, SQLAlchemy, ActiveRecord. So developers assume the whole data layer is fine. But almost every project has raw queries somewhere. Complex reporting. Performance-critical joins. Database-specific features the ORM does not expose cleanly. Those are where this hides.

# Find raw template literal queries in Node.js
grep -rn 'db\.query(`\|pool\.query(`\|client\.query(`' ./src

# Find f-string queries in Python
grep -rn 'cursor\.execute(f' ./
grep -rn 'connection\.execute(f' ./
Enter fullscreen mode Exit fullscreen mode

Run that in any codebase larger than a few weeks old. You will find something.

I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags these patterns before I move on. That said, even a basic pre-commit hook with semgrep and gitleaks will catch most of what's in this post. The important thing is catching it early, whatever tool you use.

Top comments (0)