上周帮朋友排查一个老系统的漏洞,输入账号密码的地方随便填个 ' or 1=1 -- 就直接登录成功了。这种在十几年前就被反复强调的SQL注入问题,到现在依然在不少系统里躺平——不是开发者不知道,而是总有人抱着“业务紧先上线”“我们系统没人关注”的侥幸心理。今天就从实际案例出发,把SQL注入的来龙去脉和防御手段讲透,避免再有人踩坑。
一、从一个真实案例说起:一行代码引发的数据泄露
前两年某电商平台被曝用户信息泄露,事后排查发现问题出在商品搜索功能上。开发者写的查询代码是这样的(简化版):
String sql = "SELECT * FROM goods WHERE name LIKE '%" + userInput + "%'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
正常情况下用户输入“手机”,SQL会变成 SELECT * FROM goods WHERE name LIKE '%手机%',完全没问题。但有人输入了 %' UNION SELECT id,username,password,1 FROM user --,这行代码就直接“叛变”了——最终执行的SQL变成了拼接后的恶意语句,把用户表的账号密码全查了出来。
更要命的是,这种攻击根本不需要高深技术,网上随便搜搜就能找到现成的注入语句。一旦数据库权限没做限制,攻击者甚至能执行 DROP TABLE 这样的毁灭性操作,哭都来不及。
二、SQL注入的本质:把用户输入变成“代码”执行
很多人觉得SQL注入是“黑客专属技巧”,其实本质特别简单:开发者没分清“数据”和“代码”的边界。
SQL语句是开发者写的代码,但其中包含了用户输入的数据。如果直接把用户输入拼接到SQL里,当用户输入的内容包含SQL关键字(比如OR、UNION、–等)时,数据库就会把这些输入当成SQL代码的一部分来执行,而不是单纯的数据。
举个最常见的登录场景,正常SQL是:
SELECT * FROM user WHERE username='张三' AND password='123456'
如果攻击者在用户名输入框填 ' or '1'='1,密码随便填,拼接后的SQL就变成了:
SELECT * FROM user WHERE username='' or '1'='1' AND password='xxx'
因为 '1'='1' 永远为真,这个查询会返回所有用户数据,系统自然就认为登录成功了。这就是最基础的“永真式注入”,也是最容易被忽略的攻击方式。
三、哪些场景最容易被攻击?别踩这些坑
不是所有用户输入都会引发注入,以下这些场景是高危地带,必须重点防控:
-
登录/注册模块:用户名、密码输入框是攻击者的首选目标,毕竟直接关联用户数据。
-
搜索功能:像前文提到的商品搜索、内容搜索,很多开发者会直接把搜索关键词拼接到SQL的LIKE语句里。
-
URL参数传递:比如
http://xxx.com/detail?id=123,如果后端直接用id参数拼SQL,攻击者把id改成123 OR 1=1就能获取所有详情数据。 -
后台数据筛选:比如按时间、状态筛选数据,若筛选条件直接来自前端输入且未处理,就可能被注入。
这里要特别提醒:不要以为“只允许数字输入”就安全。曾经有个订单系统,订单号是数字类型,后端用 SELECT * FROM order WHERE id= + 订单号 拼接SQL。攻击者在订单号输入框填 1 UNION SELECT credit_card FROM user,一样把信用卡信息扒走了——数字类型的参数同样会引发注入。
四、防御SQL注入:核心就做这3件事
SQL注入虽然危害大,但防御起来并不复杂,核心原则就是“切断用户输入与SQL代码的关联”,具体落地就靠这三招,按优先级排序:
1. 用预处理语句(PreparedStatement)替代字符串拼接——最有效
这是防御SQL注入的“银弹”,几乎能抵御所有基础注入攻击。原理是先把SQL语句的“骨架”(固定部分)传给数据库编译,再把用户输入作为“数据”传进去,数据库只会把输入当成纯数据处理,不会解析成SQL代码。
还是以登录为例,正确的代码应该这样写(Java示例):
String sql = "SELECT * FROM user WHERE username=? AND password=?"; // 用?占位
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, userInputUsername); // 第一个?传用户名
pstmt.setString(2, userInputPassword); // 第二个?传密码
ResultSet rs = pstmt.executeQuery();
不管用户输入什么内容,都会被当成用户名或密码的一部分,再也不会出现“把输入变成SQL代码”的情况。
这里要注意:不要手动拼接占位符的值。比如有人图省事写 "SELECT * FROM user WHERE username='" + ? + "'",这和直接拼接用户输入没区别,等于白做。
2. 做输入验证——辅助防御
预处理语句是“底线防御”,输入验证则是“前置过滤”,能把大部分恶意输入直接挡在门外。具体做法分两种:
-
类型验证:比如订单号、用户ID必须是数字,就用int/long类型接收,或者用正则表达式
^[0-9]+$校验,不符合直接拒绝。 -
关键字过滤:对搜索、评论这类允许字符串输入的场景,过滤掉OR、UNION、–、DROP等SQL关键字,或者把单引号’转义成’'(数据库会把两个单引号当成一个普通字符处理)。
但要注意:输入验证不能替代预处理语句。攻击者有很多方法绕过过滤,比如把OR写成oR、OR(全角字符),过滤机制很容易漏判,预处理语句才是最可靠的。
3. 限制数据库权限——降低攻击损失
就算前两道防线都被突破,限制数据库权限也能把损失降到最低。很多开发者图方便,给应用程序的数据库账号分配了root/sa这样的超级权限,这就等于给攻击者递了“万能钥匙”。
正确的做法是“最小权限原则”:
-
查询商品、用户信息的功能,只给SELECT权限;
-
用户注册功能,只给INSERT权限;
-
后台管理功能,按需分配UPDATE权限;
-
绝对禁止给应用程序的数据库账号分配DROP、ALTER等权限。
这样就算攻击者注入了恶意SQL,没有对应的权限也执行不了,最多只能查一些允许访问的数据,不会造成毁灭性损失。
五、最后:别让“侥幸心理”成为漏洞的温床
SQL注入不是什么高深的技术漏洞,防御方法也早已成熟,但为什么至今还有大量系统中招?核心原因不是技术问题,而是态度问题——“业务赶工期,先上线再说”“我们是内部系统,没人会攻击”“以前没出过问题,应该没事”。
数据安全没有“侥幸”可言,一次注入攻击可能导致用户信息泄露、商业数据被盗,甚至引发法律风险(《网络安全法》明确要求企业保障用户数据安全)。
下次写代码的时候,别再直接拼接SQL了,多写几行预处理语句;上线前,用 ' or 1=1 -- 这样的简单语句测一测登录、搜索功能;定期给老系统做漏洞排查,把SQL注入这样的“老问题”彻底清掉。
毕竟,保护数据安全,就是保护自己的饭碗和用户的信任。
267

被折叠的 条评论
为什么被折叠?



