根本原因是用户输入被原封不动拼入sql语句,数据库不区分来源;参数化查询强制分离结构与数据,而转义、校验、parseint等均为补丁式防护,无法替代;动态字段须白名单硬编码。

拼接字符串让用户输入变成SQL代码执行
根本原因不是“用了字符串”,而是程序把 req.query.id、req.body.username 这类用户输入,原封不动塞进了 SQL 语句里。数据库引擎看到的是完整语句,它不区分哪部分来自代码、哪部分来自用户——只要语法合法,就照单执行。
比如这行错代码:query(`SELECT * FROM users WHERE id = ${req.query.id}`),当请求是 ?id=1 OR 1=1,最终执行的就是:SELECT * FROM users WHERE id = 1 OR 1=1,结果查出全表数据。
为什么转义或校验不能替代参数化
很多开发者试图用 mysql.escape()、正则过滤数字、或 parseInt() 来“加固”拼接逻辑,但这些全是假安全:
-
parseInt('1 OR 1=1')返回1,看似没问题;但parseInt('1; DROP TABLE users--')同样返回1,而如果拼接位置在ORDER BY后,这种过滤完全无效 -
mysql.escape()只处理字符串类型,对数字型注入(如id=1 OR 1=1)不起作用;且一旦你把它和模板字符串混用,比如`WHERE id = ${mysql.escape(req.query.id)}`,等于白忙活 - 任何手动拼接 + “事后补救”的组合,都意味着 SQL 字符串在构造时已暴露可控变量,攻击面始终存在
mysql2 的 pool.query() 不等于自动防护
这是 Express 项目中最常见的认知偏差:以为用了 mysql2 就安全了。实际上:
-
pool.query('SELECT * FROM users WHERE id = ?', req.query.id)❌ 错误:第二个参数必须是数组或对象,req.query.id是字符串或数字,不触发参数化 -
pool.query('SELECT * FROM users WHERE id = ?', [req.query.id])✅ 正确:显式传入数组,驱动才走预处理路径 -
pool.execute()更可靠,它强制走预编译流程,但需连接池启用namedPlaceholders: true才支持:id写法
动态结构字段(如 ORDER BY、表名)必须白名单硬编码
占位符 ? 和 :name 只能代入值,不能代入字段名、表名、排序方向(ASC/DESC)。所以这类地方哪怕用了参数化,照样会中招:
- 错误:
ORDER BY ${req.query.sort}或ORDER BY ?—— 数据库不支持该位置参数化 - 正确做法:定义允许的排序字段白名单,如
const validSorts = ['name', 'created_at', 'score'],再用if (!validSorts.includes(req.query.sort)) throw new Error('invalid sort') - 更稳妥的是直接映射:
const sortField = { name: 'name', date: 'created_at' }[req.query.sort] || 'id',然后硬编码进 SQL
真正危险的从来不是“会不会写参数化”,而是“有没有意识到每个 SQL 字符串的每一处拼接点,都是潜在的执行入口”。只要还存在一行 WHERE id = ' + req.query.id 类写法,无论加多少层中间件、日志过滤或前端限制,漏洞就在那里。










