必须使用参数化查询(?占位符)是防止sql注入的唯一可靠方式;表名列名等标识符须用escapeid()转义或白名单校验;数据库账号应遵循最小权限原则;错误信息严禁暴露给前端。

所有用户输入都必须走 ? 占位符
参数化查询不是“更安全的选项”,而是唯一可接受的路径。任何把变量直接拼进 SQL 字符串的行为,无论类型、来源或是否经过 parseInt() 处理,都会留下注入缺口。
-
connection.query('SELECT * FROM users WHERE id = ' + userId)—— 危险,哪怕userId是数字 -
connection.query('SELECT * FROM users WHERE name = ?', [name])—— 正确,驱动层自动做类型绑定和转义 - 占位符只保护值,不能用于表名、列名、
ORDER BY字段或GROUP BY表达式 - Node.js 中用
mysql2比官方mysql包更推荐:支持 Promise、流式结果、更严格的类型处理
动态表名或列名必须用 connection.escapeId()
当业务确实需要切换分表(如按月存日志)或租户隔离(tenant_001.users),就不能靠 ?——它只认值,不认标识符。
-
connection.escapeId('user`--')→`user``--`,反引号包裹 + 内部反引号转义,防住user`; DROP TABLE x; --类 payload -
connection.escape()不能用于表名:它只对字符串值加引号并转义,对标识符无效 - 更稳妥的做法是白名单校验:
if (!['logs_202606', 'logs_202607'].includes(tableName)) throw new Error('Invalid table') - 不要在生产环境依赖 escapeId 单独兜底,它只是最后一道防线,不是替代方案
数据库账号权限必须砍到只剩 SELECT/INSERT/UPDATE
即使代码 100% 用了参数化查询,一个拥有 DROP 或 FILE 权限的账号,也能让攻击者绕过应用逻辑直读服务器文件、删库、写 shell。
- 执行
SHOW GRANTS FOR 'app_user'@'%',确认输出里没有DROP、CREATE、LOAD DATA、EXECUTE、FILE - 禁止
GRANT ALL ON *.*;应明确限定库+表:GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'app_user'@'10.0.2.%' - 开发环境用
root是常见疏忽,上线前必须切到专用账号,且该账号不应允许从任意 IP('%')登录 -
sql_mode = STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION要写进my.cnf,避免非法数据静默插入后引发后续逻辑错乱
错误信息绝不能透出给前端
MySQL 默认报错如 You have an error in your SQL syntax 或 Unknown column 'pwd_hash' in 'field list',等于把表结构、字段名甚至查询模板直接送给攻击者。
- Node.js 中必须捕获
err并丢弃原始消息:if (err) { console.error(err); res.status(500).json({ error: 'Request failed' }); } - 绝对避免
res.json({ error: err.message })或直接throw err后未拦截 - PHP 中关掉
display_errors = Off,用error_log()记完整错误,但只返回通用提示 - 前端看到的错误,和日志里记下的错误,必须是两个不同层级的信息——前者模糊,后者完整
真正难的不是写对一条 SELECT,而是在所有分支路径(包括异常、重试、缓存穿透、分库路由)里,始终守住 ? 和 escapeId() 的边界,同时不让权限和错误处理变成摆设。











