如何防范SQL注入攻击

 人参与 | 时间:2026-10-17 07:08:45

2019年某头部电商平台被拖库,何防泄露了上亿条用户记录。注入事后溯源,攻击攻击入口不是何防什么高深漏洞,就是注入一个没做参数化查询的搜索接口。前端传进来的攻击关键词被直接拼进SQL语句,攻击者构造一个 ' OR 1=1 --,何防整张用户表就出来了。注入这种事每年都在发生,攻击说到底不是何防技术难题,是注入开发习惯问题。

SQL注入的攻击本质就一句话:用户输入被当成了代码执行。数据库分不清哪个字符是何防数据、哪个是注入命令,因为你把两者混在一起交给了它。攻击理解这一点,后面所有防范手段都是围绕"让数据和指令分开"来做文章。

参数化查询,没有商量余地

这是最有效的手段,几乎没有之一。看两段代码对比就明白了。

拼接方式:

String sql = "SELECT * FROM users WHERE name = '" + userName + "'"; Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql);

参数化方式:

String sql = "SELECT * FROM users WHERE name = ?"; PreparedStatement ps = connection.prepareStatement(sql); ps.setString(1, userName); ResultSet rs = ps.executeQuery();

区别在哪?参数化查询是先编译SQL模板,再把用户输入作为纯数据填入占位符。数据库从头到尾都知道 ? 的位置是数据,不管传进来什么内容,都不会被解析成SQL语法。即使用户输入 ' OR 1=1 --,数据库也只会把它当成一个普通的字符串去匹配,找不到就返回空结果。

有人觉得用存储过程也一样安全。不一定。如果存储过程内部还在拼字符串,照样中招。关键不在于你用Statement还是PreparedStatement还是存储过程,关键在于用户输入有没有被拼进SQL文本。我见过不少项目用了PreparedStatement,但动态表名、动态排序字段还是靠字符串拼接——这些地方同样是注入点,因为占位符替代不了表名和列名。

输入验证有用,但别当主力

很多人第一反应是过滤特殊字符,把单引号、分号、注释符都干掉。这思路不是完全没用,但只能算辅助手段。原因很简单:攻击载荷的变形方式太多了。URL编码、十六进制编码、大小写混写、用 CHAR() 函数绕过关键词检测——你堵得住一种,堵不住所有。而且过度过滤会误伤正常输入,用户名字里带个单引号(比如 O'Brien)就被拦了,体验很差。

输入验证真正该做的是白名单。比如一个参数预期是整数ID,那就强制转成int,非数字直接拒绝。预期是日期,就用日期解析函数校验格式。这种做法不依赖黑名单,不会漏,也不会误伤。但它只适用于你知道输入应该长什么样的场景,像搜索框这种自由文本输入,白名单就无能为力了。

最小权限,兜底的保险

假设参数化查询漏了一处,输入验证也没拦住,那最后一道防线就是数据库账号的权限。很多应用的数据库连接用的是root或sa,一旦注入成功,攻击者能读所有库、写所有表、甚至执行系统命令。完全没必要给这么大权限。

一个Web应用只需要访问它自己那几个库,那就只授权那几个库。只需要读的表就别给写权限。不需要 DROP、FILE、xp_cmdshell 这些功能就禁掉。这么做不能阻止注入,但能把损失控制住。我见过一个案例,攻击者注入了但数据库账号只有只读权限,最后只泄露了一张商品分类表,用户数据安然无恙。

ORM不是免死金牌

现在用MyBatis、Hibernate、SQLAlchemy的团队很多,觉得用了ORM就安全了。大部分情况确实如此,但ORM都提供了原生SQL的执行通道。MyBatis里写 ${ } 而不是 #{ },就是直接拼接。Hibernate里用 createQuery("FROM User WHERE name = '" + name + "'"),同样中招。工具本身没问题,问题出在使用工具的人身上。

还有一个容易忽略的点:WAF。WAF能挡掉一些自动化扫描工具发来的低级payload,但对付手工构造的绕过 payload 效果有限。把WAF当唯一防线,跟只锁大门不锁窗户差不多。它可以加,但别指望它。

说到底,防SQL注入不是什么需要高深技术的事。参数化查询是标准做法,输入验证做补充,数据库权限收窄做兜底。三层叠上去,基本就没什么可钻的空子了。剩下的就是代码审查时多留个心眼——看到字符串拼接SQL,不管三七二十一,先标记出来再说。

顶: 99169踩: 54215