SQL 注入完全防御 2026
SQL 注入是 OWASP 自第一版以来从未跌出过 Top 3 的老牌漏洞。它的原理简单到一句话就能说清:把用户输入直接拼接进 SQL 语句,让数据库按攻击者的意图执行。但 25 年过去了,这个漏洞依旧高频出现在生产事故中。2024 年某头部 SaaS 因 ORDER BY 字段未参数化导致 280 万条用户记录被脱库,2025 年某省级政务平台因报表模块二阶注入造成大规模数据泄露——这些都不是新鲜手法,而是老问题在新场景下重演。本文系统梳理 SQL 注入的所有变体(经典、布尔盲注、时间盲注、报错注入、堆叠注入、二阶注入),逐一讲解现代防御方案:预编译语句、ORM 正确姿势、存储过程的真伪安全、最小权限设计、WAF 与审计日志,最后复盘三起真实泄露事件,给出可落地的检查清单。
一、SQL 注入的六种变体
经典注入(in-band)是最直观的形式,攻击 payload 直接通过页面回显数据。例如登录框输入 admin' OR '1'='1,绕过密码校验。UNION 注入则是借助 UNION SELECT 拼接其他表数据,把用户表内容回显到原本的列表页。这类注入在现代框架下越来越少,因为页面通常不再原样回显数据库错误,但接口直接 JSON 返回的场景仍会触发。
布尔盲注与时间盲注用于页面无回显的场景。布尔盲注通过 ' AND substring(password,1,1)='a 这种条件判断,根据页面返回内容差异逐字符还原数据;时间盲注用 sleep(5) 让数据库延迟返回,根据响应时间判断条件真假,逐位猜出敏感字段。报错注入借助 updatexml、extractvalue 等函数把数据塞进报错信息回显。堆叠注入(stacked queries)允许一次执行多条 SQL,危险性极高,幸而 PHP-MySQL、Java-PreparedStatement 默认禁用。二阶注入(second-order)是最隐蔽的一类,将在第七节单独展开。
二、预编译语句:防御的第一原则
预编译语句(prepared statement)是 SQL 注入防御的银弹。它把 SQL 模板与参数分两次发给数据库:模板先编译为执行计划,参数仅作为数据填入对应位置,绝不会被解析为 SQL 语法。无论用户输入什么,都只是字符串数据,无法改变查询结构。各语言的等价 API:Java 的 PreparedStatement、Python 的 cursor.execute(sql, params)、Node 的 mysql2.execute、Go 的 db.QueryContext + ?。
常见误区有三个。第一,emulate prepares:MySQL 驱动默认在客户端模拟预编译,把参数转义后拼回 SQL,仍然由服务端解析。在某些字符集与转义规则配合下存在绕过,建议显式 ?useServerPrepStmts=true&useLocalSessionState=true 关闭模拟。第二,参数仅用于值,不能用于表名、列名、关键字。第三,IN 子句不能直接传数组,需要按数组长度动态生成相应数量的占位符再批量绑定。每一处违反都是潜在注入点,code review 时务必扫一遍。
三、ORM 的安全边界
主流 ORM(Prisma、SQLAlchemy、Hibernate、TypeORM、GORM、Sequelize)都默认走预编译,常规 CRUD 是安全的。但所有 ORM 都提供了 raw query 通道,让开发者直接写 SQL,这就是注入复发的高发地。典型危险写法:repository.query("SELECT * FROM user WHERE name = '" + name + "'"),看似 ORM 实则手动拼接,与 mysqli_query 没有任何区别。
动态字段是 ORM 用户最容易踩的坑。例如做后台管理,用户能选择 ORDER BY 字段与升降序——很多人写成 .orderBy(req.query.sort, req.query.dir),参数直接进 SQL 模板。正确做法是把允许的字段写成常量白名单,用户输入只能是白名单中的字符串,不在则丢 400 错误。同样的逻辑适用于 GROUP BY、SELECT 列、HAVING、表名分库分表。本站的 SQL 格式化工具 可以帮助审计动态生成的 SQL 语句。
四、存储过程的真伪安全
很多老师傅推荐用存储过程对抗注入,逻辑是:业务代码只能调用已定义的过程,无法直接执行 SQL,自然没有拼接空间。在 SP 内部用静态 SQL + 参数,确实安全。问题是大量 SP 内部用的是 EXEC + 字符串拼接(动态 SQL),sp_executesql 不带参数化的写法实际上把注入风险从应用层挪到了数据库层,而且更难审计。
SQL Server 的 sp_executesql 必须配合 @p1, @p2 参数定义才能算预编译;MySQL 的 PREPARE FROM 同样如此;Oracle 的 EXECUTE IMMEDIATE ... USING 才是参数化形式。SP 优于业务代码的真正价值在于权限隔离:应用账号只能 EXECUTE 特定过程,不能直接 SELECT 表,dba 集中审查所有 SP。如果团队没有 DBA、SP 数量多变,强行用 SP 反而是负担。2026 年云数据库时代,这套范式已不再主流,预编译 + ORM + 最小权限是更好的组合。
五、最小权限与数据库分账号
哪怕代码完全没有注入,只要应用账号权限过大,事故影响也会被放大。Web 应用账号的标准配置:只对必需的业务表授予 SELECT/INSERT/UPDATE/DELETE,禁用 DROP/CREATE/ALTER/INDEX,禁用 FILE(避免 LOAD_FILE 读系统文件)、SHELL/EXEC(避免命令执行)、SUPER(避免修改全局变量)。读写分离场景,前端只读副本账号没有写权限,写入只走特定网关。
更进一步的做法是按业务域分账号:用户中心一个账号、订单中心一个账号、报表一个只读账号、审计一个只读账号。即使其中一个被打穿,也不会全库失守。云数据库时代再叠加 IP 白名单与 VPC 隔离,攻击者拿到 RCE 也无法直接连库。这套设计本身就是 zero-trust 思想在数据层的落地。本站 数据库分库分表 一文进一步讨论了多账号架构的取舍。
六、WAF、审计日志、漏洞扫描
WAF(Web Application Firewall)是 SQL 注入的第二道闸。Cloudflare、阿里云、AWS WAF 默认规则集都包含数千条 SQL 注入特征,能拦截绝大多数自动化扫描器。但 WAF 不是银弹:攻击者用 0x、十六进制、注释拆分、大小写混淆、JSON 嵌套、Unicode 编码等手段绕过特征早已是渗透必修课。WAF 的价值在减少噪音、给应急响应争取时间,绝不能取代代码层防御。
审计日志是事后定位的关键。慢查询日志、binlog、应用层 SQL 日志至少保留 90 天,关键操作日志保留 1 年。一旦怀疑被注入,立刻全表 LIKE 关键字段(password、phone、id_card)出现频率,结合时间窗口与 IP 聚类找出可疑会话。漏洞扫描层面,SQLMap 是行业事实标准,CI 中集成 OWASP ZAP、Burp Pro、Acunetix 做回归扫描,每月跑一次全量。本站的 PIPL 与 GDPR 合规 一文讨论了相关日志保留与个人信息保护的平衡。
七、二阶注入:最容易漏的一类
二阶注入特指数据先合法入库、后续被取出再拼接到 SQL 时触发。例:注册时昵称 admin'-- 经过转义存入数据库(存的就是字面字符串);管理后台导出 Excel 时 SELECT * FROM logs WHERE user='" + nickname + "' 直接拼接,注入触发。开发者只在入口处做了校验,忘了所有从数据库出来的字段同样要视为外部输入。
防御原则:永远不要在拼接 SQL 时对来源做差异化处理。无论字符串来自请求参数、Cookie、Header、第三方接口、还是数据库读取,都必须走预编译或参数化。Code review 关注三类危险关键字:拼接 + 字符串、format + sql、f-string + sql。SAST 工具(Semgrep、CodeQL)可以高效找出这种模式。配合白盒渗透测试针对性地构造 admin'-- 这种无回显数据,看后续报表是否触发,一旦发现立即修复全链路。
八、真实数据泄露事件复盘
事件一(2024,某北美 SaaS):报表模块允许用户自定义 SELECT 列,参数未做白名单。攻击者通过 columns=email,(SELECT password FROM users LIMIT 1) AS x 把密码 hash 拖出。教训:动态列名必须走白名单,永远不要相信前端传的字段名。事件二(2025,某省级政务):表单提交存入数据库,三天后定时任务汇总报表时把昵称拼接到 SQL,触发二阶注入,泄露 50 万公民信息。教训:所有出库字段都必须重新视为外部输入。
事件三(2024,某加密交易所):客服后台搜索框使用 ORM 但用了 raw 拼接,攻击者通过钓鱼邮件拿到内部账号后利用注入读取冷钱包私钥,损失 8000 万美元。教训:ORM 不等于安全,最小权限缺失会让任何小漏洞演变为大事故。三起事件共同点:代码 review 不严、SAST 缺失、权限过大、缺乏二阶注入测试。把这四点写进上线 checklist,能挡住 90% 的潜在事故。SQL 注入永远不会消失,但完全可以通过工程化手段降到可控水平。
常见问题
用 ORM 还会发生 SQL 注入吗?
会。ORM 默认使用预编译语句是安全的,但开发者经常退化到 raw query、原生 SQL 拼接、orderBy 字段动态拼接、in 子句字符串拼接等场景,这些都是经典注入点。Prisma、SQLAlchemy、Hibernate、TypeORM 都出过 CVE。结论:ORM 不是免死金牌,所有动态 SQL 都要审计,所有用户输入都要参数化。
预编译语句一定能防住注入吗?
能防住绝大多数注入,但也有例外。表名、列名、ORDER BY 的方向、LIMIT 数量这些位置不能用占位符,只能拼接。这种情况要走白名单:把允许的字段写在常量数组里,用户输入只能是数组中的值。MySQL 的 prepare 模拟模式(默认开启)在某些版本下也存在转义陷阱,建议显式关闭 emulate prepares。
什么是二阶 SQL 注入?
二阶注入是指恶意 payload 第一次入库时被正常存储(甚至已转义),但在后续某次取出后再拼接到 SQL 中执行。典型场景:注册时昵称写入数据库,后台报表把昵称取出拼到 SQL 中查询统计。开发者只防了入口转义,忘了出口同样要参数化。任何从数据库取出的字符串拼回 SQL 都属于二阶风险,必须同样使用预编译。
WAF 能完全替代代码层防御吗?
不能。WAF 基于特征匹配,攻击者用编码、注释拆分、大小写混淆、JSON 嵌套等技巧绕过早已是常规操作。WAF 的价值是减少噪音、阻挡自动化扫描、为应急响应争取时间,但绝不能作为唯一防线。Cloudflare、阿里云 WAF 实测可拦截 90% 以上自动化 payload,对针对性攻击仅 30-50%。代码层防御是底线,WAF 是加成。
数据库账号该用什么权限?
严格遵循最小权限。Web 应用账号只授予 SELECT/INSERT/UPDATE/DELETE 必需表的权限,禁用 DROP、ALTER、CREATE、FILE、SHELL 等高危权限。读写分离环境下,前台只读账号没有写权限,后台写账号有限制 IP。审计与归档用专用只读账号。即使发生注入,攻击者拿到的也只是有限子集,无法直接 dump 全库或执行系统命令。