当前位置:首页 > 焦点 > 防sql注入 正文

防sql注入

时间:2026-10-17 05:05:42 来源:力不从心网

2019年,注入某头部电商平台的注入安全团队做了一次内部演练。他们用一款开源扫描器对自家登录接口发起测试,注入不到三分钟,注入一个拼接用户输入的注入后端查询就被攻破了。攻击者构造的注入用户名是 ' OR '1'='1,密码随便填。注入系统返回了管理员的注入登录态。

这不是注入电影桥段,是注入真实发生过的漏洞。SQL注入的注入本质很简单:应用程序把用户输入当成了代码来执行。数据库分不清哪部分是注入开发者写的指令,哪部分是注入用户填的数据——只要拼接字符串,这个模糊地带就永远存在。注入

为什么“过滤关键词”这条路走不通

很多人第一反应是注入写个黑名单,把 SELECT、UNION、-- 这些词过滤掉。我见过一个项目组这么干过,结果攻击者把关键词拆成 SEL/**/ECT,用注释绕过;又或者用 URL 编码、大小写混写、十六进制编码,花样多得数不过来。你封一个,他换一个。这种猫鼠游戏,防守方永远慢半拍。

更麻烦的是,有些业务场景天然需要用户输入包含特殊字符。比如搜索功能,用户搜“O'Brien”这种带单引号的名字,你一刀切过滤,正常功能就废了。

参数化查询才是正解

根本的解法只有一个:把数据和指令彻底分开。参数化查询(Prepared Statement)干的就是这件事。数据库先编译好 SQL 模板,再把用户输入当作纯数据填入占位符。无论输入里有什么字符,它都只是数据,不会被解析成 SQL 语法。

以 Java 为例,错误写法是:

String sql = "SELECT * FROM users WHERE name = '" + userName + "'";

正确写法是:

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

差别就这么大。一个字符串拼接,一个占位符绑定。后者让注入攻击直接失效。

ORM 不是万能药

有人觉得用了 Hibernate、MyBatis 就高枕无忧了。不一定。MyBatis 里用 ${ } 拼接和用 #{ } 参数绑定,效果天差地别。${ } 是直接文本替换,等于把用户输入原样塞进 SQL。#{ } 才走预编译。我审查过一些代码,开发者图方便,在 ORDER BY 后面用 ${ } 动态拼字段名,结果这里就成了突破口。

ORM 框架的查询构造器也不是绝对安全。某些冷门方法支持原生 SQL 片段,一旦用了,防线就破了。关键是理解你用的工具底层到底做了什么,而不是盲目信任框架。

纵深防御:别只靠一层

参数化查询是底线,但真实系统里还得多加几道锁。数据库账号权限最小化——Web 应用连的账号不该有 DROP TABLE 的权限。错误信息别直接吐给前端,把堆栈和 SQL 语句暴露出去等于给攻击者送地图。WAF 可以挡掉一部分自动化扫描,但别把它当主力。

还有一点常被忽略:存储过程如果内部再拼接动态 SQL,同样会中招。安全这件事,链条上任何一个环节偷懒,整个防线就垮了。

回到开头那个案例。后来他们改成了参数化查询,同样的扫描器再跑,返回的全是语法错误——因为 ' OR '1'='1 被当成了一个普通的字符串去匹配用户名,当然匹配不到。攻击面就这么消失了。没有 magic,只是把该做的事做对了。

推荐内容