DEV Community

Cover image for 3 SQL Injection Patterns Cursor Keeps Writing Into Your API
Charles Kern
Charles Kern

Posted on

3 SQL Injection Patterns Cursor Keeps Writing Into Your API

TL;DR

  • AI editors generate SQL queries with template literals -- directly injectable, CWE-89
  • This pattern shows up in ~40% of vibe-coded backends with any database layer
  • Fix: parameterized queries everywhere, no exceptions

I was reviewing a friend's side project last week. Node.js API, React frontend, Postgres. Clean architecture, good folder structure. Then I looked at the query layer.

Every single database call was built with template literals. Not one parameterized query in the whole codebase. I ran sqlmap against the search endpoint in about 30 seconds and had a full database dump.

This wasn't my friend's oversight. Cursor wrote those queries. And it keeps writing them that way.

Pattern 1: The Classic Template Literal

You ask Cursor for a search endpoint. It gives you this:

// CWE-89 -- SQL injection via template literal
app.get('/api/users', async (req, res) => {
  const { name } = req.query;
  const users = await db.query(`SELECT * FROM users WHERE name = '${name}'`);
  res.json(users);
});
Enter fullscreen mode Exit fullscreen mode

name goes straight into the query string. A request with name='; DROP TABLE users; -- does exactly what you think it does. No parsing, no escaping, just execution.

The fix is one line:

const users = await db.query('SELECT * FROM users WHERE name = $1', [name]);
Enter fullscreen mode Exit fullscreen mode

The database driver handles escaping. The query structure is fixed at parse time. User input cannot change what the query does.

Pattern 2: String Concatenation

Older pattern, but Cursor still generates it when you ask for something simple like "fetch order by ID":

// CWE-89 via string concatenation
const order = await db.query(
  "SELECT * FROM orders WHERE id = " + req.params.id
);
Enter fullscreen mode Exit fullscreen mode

req.params.id is a string. It's never validated. If it contains SQL, it runs.

// Parameterized
const order = await db.query(
  'SELECT * FROM orders WHERE id = $1',
  [req.params.id]
);
Enter fullscreen mode Exit fullscreen mode

Even for numeric-looking IDs, always parameterize. Add type validation on top if you want -- but parameterize first.

Pattern 3: ORM Escape Hatch

This one's the sneakiest. Developers assume using Sequelize or Prisma makes them safe. It doesn't, when the AI reaches for the raw query escape hatch:

// Looks like ORM, but it's not safe
const results = await User.findAll({
  where: db.literal(`name = '${req.query.search}'`)
});
Enter fullscreen mode Exit fullscreen mode

db.literal() tells Sequelize to pass that string directly to the database unescaped. You're back to raw string interpolation, wrapped in an ORM call that makes it look safe.

// Actual safe ORM usage
const results = await User.findAll({
  where: { name: req.query.search }
});
Enter fullscreen mode Exit fullscreen mode

If you need complex conditions, Sequelize has Op.like, Op.and, Op.or. Use those. Never db.literal() with user-controlled input.

Why This Keeps Happening

Template literals and string concatenation dominate documentation, tutorials, and StackOverflow answers -- especially the older ones that LLMs were trained on. The unsafe pattern is readable. It makes the SQL structure obvious. It's what most examples show.

LLMs pattern-match to what appeared most in training data. Parameterized queries show up less frequently there, because tutorial authors optimize for clarity, not security. The "security hardening" section at the bottom of a tutorial is what fewer people read, click on, and link to -- so it's underrepresented in training data.

The model isn't choosing to generate insecure code. It's completing a token sequence that matches what it learned.

Catching It Before It Ships

Two grep commands that catch most of what Cursor generates:

# Template literal SQL
grep -rn 'query.*\`.*\${'  ./src

# String concatenation in queries
grep -rn 'query.*+.*req\.' ./src
Enter fullscreen mode Exit fullscreen mode

Run these in pre-commit. They won't catch everything, but they catch the most common AI-generated patterns.

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)