参数化查询是防SQL注入唯一可靠方式:必须用占位符(如?或$1)配合参数数组,严禁字符串拼接;表名/字段名等结构部分须白名单校验后硬编码,不可参数化。

必须用参数化查询,不能拼接字符串——这是唯一可靠且可落地的解法。
为什么字符串拼接req.body.username直接进SQL就是高危操作
Node.js里最典型的漏洞写法是:const query = `SELECT * FROM users WHERE username = '${req.body.username}'`。这种写法会让攻击者输入' OR 1=1 --后,实际执行的变成SELECT * FROM users WHERE username = '' OR 1=1 --',绕过所有验证逻辑。
问题不在数据库驱动,而在你让输入内容参与了语句结构构造。只要没走预编译流程,任何转义、正则过滤、trim或toLowerCase都挡不住精心构造的payload。
- MySQL、PostgreSQL、SQLite等所有关系型数据库驱动(如
mysql2、pg、sqlite3)都原生支持参数化查询,不是“可选功能”,而是默认安全路径 - ORM如
Sequelize或TypeORM底层也靠它实现安全,但一旦你调用.query()或.execute()传原始SQL字符串,就等于手动拆掉防护 - 错误认知:“我用了
escape()”或“我过滤了单引号”——这些在多字节编码、Unicode边界、注释符嵌套等场景下全部失效
mysql2和pg中参数化查询的正确写法
不同驱动语法略有差异,但核心都是「占位符 + 参数数组」分离结构,由驱动完成绑定与类型转换。
以mysql2为例:
const sql = 'SELECT * FROM users WHERE username = ? AND status = ?';
db.execute(sql, [req.body.username, 'active'], (err, results) => {
// 安全:参数不会被解析为SQL结构
});
以pg(PostgreSQL)为例:
const sql = 'SELECT * FROM users WHERE email = $1 AND age > $2';
client.query(sql, [req.body.email, req.body.minAge], (err, res) => {
// $1、$2 是位置占位符,非字符串插值
});
- 不要用
db.query('SELECT ...' + req.body.xxx),哪怕加了escape()也不行 - 不要用模板字符串拼接参数,
`WHERE id = ${id}`和'WHERE id = ' + id一样危险 - 如果必须动态字段名(如排序字段),只能走白名单校验:
if (!['name', 'email', 'created_at'].includes(req.query.sort)) throw new Error('Invalid sort field')
ORM里最容易被忽略的两个危险点
用Sequelize或TypeORM不等于自动免疫。很多团队在“安全假象”中翻车。
第一个坑:raw query绕过ORM防护
// 危险!完全暴露给注入
await sequelize.query(`SELECT * FROM users WHERE name = '${req.body.name}'`);
<p>// 正确:仍需参数化
await sequelize.query('SELECT * FROM users WHERE name = ?', { replacements: [req.body.name] });
第二个坑:where对象里混入用户控制的键名
// 危险:如果 req.body.filter 是 { '$ne': 'admin' },可能触发NoSQL式逻辑篡改
User.findAll({ where: req.body.filter });
<p>// 正确:只允许预定义字段 + 显式操作符白名单
const allowedFields = ['username', 'email', 'status'];
const allowedOps = ['$eq', '$in', '$like'];
if (!allowedFields.includes(Object.keys(req.body.filter)[0])) throw error;
- ORM的
findAll({ where: { ... } })本身安全,但若where对象来自未校验的req.body,就可能被注入$ne、$regex等操作符(尤其在搭配MongoDB时更致命) - 别依赖ORM“自动过滤”,它只保证你写的静态
where安全;动态结构必须自己守门
输入验证只是辅助,不能替代参数化查询
用Joi或Zod校验req.body.username是好习惯,但它解决的是业务逻辑合规性,不是SQL安全。
例如:
- 一个邮箱字段通过
Joi.string().email()校验,不代表它不能被用于UNION SELECT攻击(如果后面还拼SQL) - 年龄字段限制
min(18),但若拼进SQL:AND age > ${req.body.age},攻击者仍可输18; DROP TABLE users--(在支持多语句的驱动中) - 验证能拦住明显非法输入,但拦不住“合法格式 + 恶意语义”的组合,比如
admin' --对isAlphanumeric()仍是合法字符串
真正关键的防线只有一条:用户输入永远不参与SQL语句结构生成。其余所有手段——CSP头、WAF、日志审计、权限最小化——都是在它失守后的补救。


















