mysql2.query()的?占位符必须用数组传参,不支持命名参数;postgresql需用$1等数字占位符且顺序严格对应;表名、列名、order by等结构部分须白名单校验,不可参数化。

mysql2.query() 的 ? 占位符必须用数组传参,不能拼字符串
参数化查询不是“用了占位符就安全”,而是必须严格遵循绑定方式。mysql2 的 query() 方法只认 ? 占位符,且参数必须是数组——哪怕只有一个值。
- ✅ 正确:
connection.query('SELECT * FROM users WHERE id = ?', [req.query.id]) - ❌ 错误:
connection.query(`SELECT * FROM users WHERE id = ${req.query.id}`)—— 任何输入都可能变成1 OR 1=1 -- - ⚠️ 注意:
mysql2不支持命名参数(如:id或@id),写成WHERE id = :id会导致参数完全不绑定,查不到数据也不报错 - ⚠️ 常见漏点:日志里打印原始 SQL 时直接拼接
req.query.id,哪怕查询本身安全,日志也成了注入入口
PostgreSQL 用户必须用 $1,且顺序不能错
pg 驱动不识别 ?,只接受数字位置参数 $1、$2……而且顺序和传入数组严格一一对应。
- ✅ 正确:
client.query('SELECT * FROM users WHERE status = $1 AND age > $2', ['active', 18]) - ❌ 错误:
client.query('SELECT * FROM users WHERE status = $1', ['active', 18])→ 报错error: bind message supplies 2 parameters, but prepared statement "" requires 1 - ⚠️ 性能提示:
pg默认开启预编译,首次执行后缓存执行计划;但如果 SQL 模板频繁变动(比如动态字段列表),反而增加解析开销 - ⚠️ 安全盲区:错误信息里若暴露了原始 SQL 字符串(如
console.error(err.stack)),攻击者可能看到未过滤的用户输入
表名、列名、ORDER BY 子句不能参数化,必须白名单硬校验
SQL 协议规定,占位符只能出现在值的位置(WHERE、VALUES、SET),不能用于结构部分。这是协议限制,不是驱动缺陷。
- ✅ 正确:
const allowedSortFields = ['name', 'email', 'created_at']; if (!allowedSortFields.includes(req.query.sortBy)) throw new Error('Invalid sort field');再拼接:ORDER BY ${req.query.sortBy} - ❌ 错误:
client.query('SELECT * FROM $1 WHERE active = $2', [req.query.tableName, true])→ 直接报语法错误 - ⚠️ Joi 无法覆盖这类场景:
joi.string().max(20)对req.query.sortBy校验毫无意义,因为;、--、/*都在合法字符范围内 - ⚠️ 动态 JOIN 表、
GROUP BY字段、UNION子句同理,全部需白名单 + 显式判断
ORM 并非万能,raw() 和 literal() 是高危出口
Sequelize、TypeORM 等默认走参数化,但它们都留了原生 SQL 接口——一旦使用,防护就失效。
- ✅ 安全:
User.findAll({ where: { email: req.body.email } })—— 内部自动参数化 - ❌ 危险:
sequelize.query(`SELECT * FROM users WHERE name = '${req.body.name}'`)—— 别名、模型定义都救不了拼接 - ⚠️ 更隐蔽的坑:
fn('LOWER', literal(req.body.q))中的literal()会跳过参数化,等同于字符串拼接 - ⚠️ 注意:
replacements只支持命名参数(:id),bind支持数组($1),混用会静默失败
真正难的不是写对一行 query(),而是确保所有分支——if/else 里的不同 SQL、错误处理中的日志输出、监控埋点里的原始 query 字符串——全都经过参数化或白名单校验。漏掉一处,整条链路就形同虚设。










