flawopen.com/sql-injection/Javascript
●
CWE-89 · 严重
安全研究 · FlawOpen
JavaScript 中的 SQL 注入(mysql2、pg、Prisma、Knex)
在 pool.query() 里使用模板字符串或 + 拼接,会让请求参数改写你的 SQL 并返回所有行(CWE-89)。本文介绍 mysql2、node-postgres、Prisma 和 Knex 的安全写法,解释为什么 ${} 在一个 Prisma 方法里安全、在另一个里却很危险,以及如何不用 orderByRaw 实现排序。[1], [2], [3], [4], [5], [6]
💡 通俗易懂的原理解析 (ELI5)
想象一个分拣中心:货物到达前,运输路线就已打印并锁定。标签上的姓名只能填入预留栏,不能给路线增加新站点。SQL 参数也是如此:mysql2 execute(sql, [userId]) 和 pg query(sql, [status]) 会绑定值;Prisma 带标签的 $queryRaw 会绑定每个 ${} 占位处。如果先用模板字符串或 + 拼好 SQL,或调用 $queryRawUnsafe,客户文本就会被写进路线本身。Knex 的 orderByRaw 对排序指令也有同样风险;把请求的键映射到固定列,再调用 orderBy。[1], [2], [3], [4], [5], [6]
核心概念与专有名词
Template literals in query()- 带有
${userId}的反引号字符串会在调用驱动之前由 JavaScript 求值,所以驱动收到的是值已经嵌在里面的完整 SQL。这种语法看起来像占位符,但什么也没有绑定。[7], [1], [6] mysql2 execute() vs query()execute(sql, values)使用服务端预处理语句;query(sql, values)在客户端转义值并替换到?标记中。只要值放在数组里,两者都安全;如果 SQL 字符串是用+或模板字符串拼出来的,两者都帮不上忙。mysql2 默认关闭multipleStatements。[3], [6], [9]pg query(text, values)- node-postgres 把
values与text分开发送,所以$1由 PostgreSQL 填充。没有值时,pg 使用简单查询协议,该协议还允许在一次调用中执行多条以分号分隔的语句。[2], [6]
框架映射
- 本指南所述弱点的 CWE 参考: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
攻击执行流程分解
步骤 2
源代码对比:漏洞与安全实现
存在漏洞的实现
// users.js:下面每个查询都用请求数据拼出 SQL 文本
// mysql2:调用驱动之前模板字符串就已求值
export async function getUser(pool, userId) {
const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
return rows[0];
}
// pg:字符串拼接,而且没有可绑定的值数组
export async function findOrders(pg, status) {
const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
return rows;
}
// Prisma:Unsafe 版本接收的是拼好的字符串
export function findByEmail(prisma, email) {
return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}
// Knex:orderByRaw 把请求中的值作为原始 SQL 插入
export function listUsers(knex, sortColumn) {
return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
加固后的安全修复
// users.js:值放进值数组,列名取自白名单
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);
// mysql2:服务端预处理语句,值绑定到 ?
export async function getUser(pool, userId) {
const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
return rows[0];
}
// pg:PostgreSQL 用值数组填充 $1
export async function findOrders(pg, status) {
const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
return rows;
}
// Prisma:标签模板,每个 ${} 都变成绑定参数
export function findByEmail(prisma, email) {
return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}
// Knex:orderBy 把白名单中的列名作为标识符加引号
export function listUsers(knex, sortKey) {
const column = SORTABLE.get(sortKey) ?? 'id';
return knex('users').select('id', 'email').orderBy(column);
}
工程与系统安全加固清单
- ✓在 CI 中用 ESLint 的
no-restricted-syntax规则或 Semgrep 禁止$queryRawUnsafe、$executeRawUnsafe、带插值的whereRaw/orderByRaw,以及query()/execute()第一个参数中的模板字符串或+。[1], [3], [2], [4], [5], [6] - ✓所有值都放进驱动的值数组(mysql2 用
?,pg 用$1),或作为 Prisma 标签模板$queryRaw中的${}传入。[3], [2], [4], [6] - ✓在路由处用 schema(zod、Joi)校验输入:ID 转为数字,拒绝未知键,在需要字符串的地方不接受数组。[8], [6]
- ✓用
Map白名单映射排序键,再把结果交给 Knex 的orderBy()或 Prisma 的orderBy;绝不把req.query传给原始 ORDER BY。[5], [4], [6] - ✓保持 mysql2 的
multipleStatements关闭,并使用没有 DDL 权限的数据库用户连接。[3], [9], [6]
参考来源
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- node-postgres — Parameterized query
- mysql2 — Using prepared statements
- Prisma — Raw queries and SQL injection
- Knex.js — Raw parameter binding
- OWASP SQL Injection Prevention Cheat Sheet
- MDN — Template literals (JavaScript)
- Express 4 API — req.query
- MySQL C API — Multiple statement execution support