flawopen.com/SQL Injection/JavaScript

SQL Injection in JavaScript

Critical CWE-89 Draft — pending review
Language: English Português (Brasil) Español Français Deutsch Русский 简体中文 日本語 हिन्दी 한국어 Bahasa Indonesia
ELI5

Imagine a form that only ever expects a ticket number, like 482. SQL injection is what happens when someone types something sneaky into that box instead of a number — a trick phrase that makes the system say "show me every ticket" instead of just ticket 482, because the system never checked that what it received was actually just a number.

Key terms on this page
user-controlled input
Any value that ultimately came from whoever is using — or attacking — the app: a form field, a URL parameter, an uploaded filename, an HTTP header. The app can't assume it's well-formed or safe.
SQL query
The command sent to a database — e.g. "get this row," "delete this table." Its meaning comes entirely from its exact text, which is what makes injecting extra text into it dangerous.

What's happening

SQL injection happens when user-controlled input gets inserted directly into a database query's text, instead of being passed as a separate value. In Node.js, the trap is especially easy to fall into because template literals are the idiomatic way to build almost every string in the language — including, by accident, SQL.

A query built with a backtick template literal that embeds a variable directly is functionally identical to string concatenation — it just doesn't look like it.

Real-world impact

In 2015, UK telecom TalkTalk suffered a breach affecting over 150,000 customers after attackers exploited a SQL injection flaw in a legacy web page inherited through a company acquisition. The UK's data protection regulator fined TalkTalk £400,000, describing the failure as preventable and basic.

Source: UK Information Commissioner's Office enforcement notice, 2016 — see References below.

Vulnerable vs. fixed

VULNERABLE
// userId comes straight from the request
async function getUser(pool, userId) {
  const query = `SELECT * FROM users
    WHERE id = ${userId}`;
  const [rows] = await pool.query(query);
  return rows[0];
}
FIXED
// value is passed separately, never inlined
async function getUser(pool, userId) {
  const [rows] = await pool.query(
    'SELECT * FROM users WHERE id = ?',
    [userId]
  );
  return rows[0];
}

Why the fix works

The fixed version passes the query text and the value to pool.query() as two separate arguments — the driver sends them to the database separately as well, so the query's structure is locked in before the value is attached to it. A template literal can't do this: by the time the string exists, the value is already indistinguishable from the rest of the query text.

JavaScript-specific gotchas

Placeholder syntax differs by driver

mysql/mysql2 use positional ? placeholders; pg (PostgreSQL) uses numbered placeholders like $1, $2. Copying a pattern from one driver's docs into the other silently breaks or, worse, silently does nothing and leaves the query unparameterized.

ORM raw-query escape hatches reintroduce the risk

Sequelize and Prisma both parameterize their normal query API automatically. Prisma even names its unsafe escape hatch explicitly — $queryRawUnsafe — distinct from the tagged-template $queryRaw, which is safe. Sequelize's sequelize.query() is safe only when you pass replacements as a separate option, not when you build the string yourself.

A template literal "looking structured" doesn't make it parameterized

Multi-line template literals read as more deliberate than a one-line concatenation, but the database driver has no idea the string came from a template literal — it just sees text. There's no automatic escaping tied to backtick syntax.

Common misconceptions

"Template literals are 'templates,' so they must be safe"

Template literals are just JavaScript's native string interpolation — the name doesn't imply any escaping or safety mechanism.

"Prisma protects me by default"

True for $queryRaw tagged templates and the standard query API — false for $queryRawUnsafe, which exists specifically to opt out of that protection.

"This value came from an authenticated user, so it's trusted"

Authentication proves who sent the value, not that the value itself is safe to embed in a query. An authenticated attacker is still an attacker.

How to check if you're affected

grep -rn "query(\`" --include="*.js" --include="*.ts" . | grep '\${' grep -rn "\$queryRawUnsafe" --include="*.ts" . grep -rn "sequelize.query(" --include="*.js" .
A linter rule (e.g. eslint-plugin-security's detect-sql-injection-style checks) run in CI catches new occurrences automatically instead of relying on periodic greps.

Prevention checklist

FAQ

Are ORMs like Prisma or Sequelize safe by default?

Yes, for their standard query methods. Both also ship an explicit raw-SQL escape hatch that is not safe by default and requires the same parameterization discipline as hand-written SQL.

Is this different from XSS?

Related pattern, different target — XSS injects into HTML/JS rendered in a browser; SQL injection injects into a database query. The underlying mistake (mixing untrusted data into a command's structure) is the same shape, but the fix and the damage are different.

Does TypeScript's type system help here?

Not directly — a string type doesn't distinguish a safe query from a dangerous one. Type safety and query-construction safety are separate properties.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: Command InjectionPath Traversal XSSInsecure Deserialization