flawopen.com/SQL Injection/JavaScript
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.
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.
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.// 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];
}
// 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];
}
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.
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.
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.
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.
Template literals are just JavaScript's native string interpolation — the name doesn't imply any escaping or safety mechanism.
True for $queryRaw tagged templates and the standard query API — false for $queryRawUnsafe, which exists specifically to opt out of that protection.
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.
grep -rn "query(\`" --include="*.js" --include="*.ts" . | grep '\${'
grep -rn "\$queryRawUnsafe" --include="*.ts" .
grep -rn "sequelize.query(" --include="*.js" .
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.
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.
Not directly — a string type doesn't distinguish a safe query from a dangerous one. Type safety and query-construction safety are separate properties.