1. 引言
在众多可能让我们彻夜难眠的安全攻击中,SQL 注入(SQL Injection)绝对值得特别关注。它是最常见的 Web 系统攻击手段之一,且已被集成进许多自动化工具中(如 sqlmap)。这使得即使技术水平不高的攻击者也能发动极具破坏性的攻击。
简单来说,SQL 注入攻击者试图将任意 SQL 代码插入到应用程序的逻辑中。如果攻击成功,这些代码将以应用程序的权限和安全身份执行。这类攻击属于远程代码执行攻击(Remote Code Execution, RCE)的一种,即上传代码(二进制或脚本)并诱骗服务器执行。
SQL 注入长期以来一直位列 OWASP 十大安全风险之中,其危害不容小觑。一次精心设计的攻击可以轻松篡改或删除应用数据,甚至抹除攻击痕迹。SQL 注入指的是攻击者尝试修改和破坏应用程序所使用的 SQL 语句。
我们之前写过一篇关于 Java 中 SQL 注入的文章,本文将更深入地探讨 SQL 注入的本质、原理及其危险性,并介绍其分类和混淆技术,以及如何防范。
2. SQL 注入是如何工作的?
一句话总结:当应用程序通过拼接字符串的方式构建 SQL 命令,并将用户输入直接拼入 SQL 语句中时,就可能产生 SQL 注入漏洞。
例如,一个简单的登录验证伪代码如下:
algorithm VulnerableLoginFormHandler(username, password):
// INPUT
// username, password = login data from an HTML form
// OUTPUT
// authOk (boolean) = authentication status
// UserId = user ID
// FullName = the name of the user
passwd_hash <- md5 of password
sql <- "select id, fullname from users where passwordHash = '" + passwd_hash
sql <- sql + "' and userName ='" + username + "'"
result <- query sql and fetch from the database
if result is an error:
return false, null, null
else:
return true, result[id], result[fullname]
正常输入(如用户名 root
,密码 root
)会生成如下 SQL:
SELECT id,Fullname FROM users WHERE Password ='63a9f0ea7bb98050796b649e85481845' AND Username ='root'
但如果用户名输入为 "root' or 1=1 limit 1;--"
,密码为空,则生成的 SQL 为:
SELECT id,Fullname FROM users WHERE Password ='d41d8cd98f00b204e9800998ecf8427e' AND Username ='root' or 1=1 limit 1;--'
这个查询将始终返回至少一条记录,绕过身份验证。
更糟的是,攻击者还可以构造更复杂的 SQL 片段,例如:
"root' or 1=1 ; select * from customers;--"
:查询其他表"root' or 1=1 ; insert into users (username, password) values ('backdoor_user','some_hash');--"
:插入数据"root' or 1=1 ; drop table customers; --"
:删除表"root' or 1=1 union select password_hash as fullname from users; --"
:窃取密码哈希
攻击手段层出不穷,具体能执行哪些操作取决于数据库结构和应用对错误信息的反馈。
SQL 注入之所以如此危险,是因为很多应用直接将用户输入拼接到 SQL 中,甚至不做任何校验。更糟的是,多数数据库支持在一条语句中执行多个 SQL 命令。即使应用不显示结果,这些命令也会被执行。
3. SQL 注入攻击的分类
SQL 注入根据攻击方式和应用响应机制可分为以下几类:
3.1. 错误型(Error-based)
攻击者利用数据库返回的错误信息进行攻击。例如,如果应用将数据库错误直接暴露给用户,攻击者可以通过错误信息了解数据库结构,从而构造更精确的注入语句。
✅ 最佳实践:应用应避免将详细的错误信息暴露给用户。
3.2. 联合型(Union-based)
攻击者使用 UNION
构造 SQL 查询,获取额外数据。此类攻击通常需要知道目标表的列数和列名。
⚠️ 注意:若应用返回 SQL 错误信息,攻击者可从中获取表结构等关键信息。
3.3. 盲注型(Blind SQL Injection)
应用不返回任何错误或结果信息时,攻击者通过布尔值或时间延迟等方式进行“猜解”。
- 布尔盲注:通过判断真假值(如页面是否返回特定内容)来获取信息。
- 时间盲注:通过
IF
和SLEEP
等函数控制响应时间,从而推断信息。
✅ 缓解建议:
- 不返回具体错误信息
- 固定响应时间,避免时间差异被利用
- 限制高频请求频率
4. 高级 SQL 注入混淆技术
有些应用通过检测输入中是否包含 SQL 关键字(如 select
、update
、delete
)来防御注入。但这远远不够。
攻击者会使用各种混淆技术绕过检测,例如:
- 大小写混合(
sElEcT
) - 转义字符(
\x53\x45\x4C\x45\x43\x54
) - 使用 ASCII 编码转换字符
示例(真实攻击片段):
GET http://www.somesite.com/Portal/showPortalPage.do?action&codItem=(cONVErt(int,(ChAR(58)%2BChAR(113)%2BChAR(101)%2BChAR(118)%2BChAR(58)%2B(selECT%2F%2A%2FTOP%2F%2A%2F1%2F%2A%2A%2FsubsTriNG((IsNull(caSt(APP..syscolumns.name%2F%2A%2FaS%2F%2ANVARChAR(4000)),ChAR(32))),1,100)%2F%2A%2A%2FFrom ...
解析后为:
GET http://www.somesite.com/Portal/showPortalPage.do?action&coditem=(
convert(int,(:qev:(select top 1 substring((isnull(cast(app..syscolumns.name as nvarchar(4000)), )),1,100) from ...
攻击者通过复杂的编码绕过关键字检测,极具隐蔽性。
5. 防御 SQL 注入:参数绑定(Parameter Binding)
如何防御这些攻击?最核心的方法是:
✅ 永远不要直接拼接用户输入构建 SQL 语句。
使用参数绑定(Parameter Binding)是现代 ORM 框架的标配。它不仅能防止 SQL 注入,还能提升数据库性能。
修改后的伪代码如下:
algorithm FixedLoginFormHandler(username, password):
// INPUT
// username, password = login data from an HTML form
// OUTPUT
// authOk (boolean) = authentication status
// UserId = user ID
// FullName = the name of the user
passwd_hash <- md5 of password
sql <- "select id,fullname from users where passwordHash = :bPass and userName = :bUser"
statement <- database.prepare(sql)
statement.bind_parameter('bPass', passwd_hash)
statement.bind_parameter('bUser', username)
statement.execute()
result <- statement.fetch_all_data()
if result is an error:
return false, null, null
else:
return true, result[id], result[fullname]
参数绑定的工作流程如下:
prepare
:发送 SQL 模板给数据库,数据库解析并缓存执行计划bind
:绑定参数值execute
:执行查询fetch
:获取结果
优点:
- 安全:参数不会被当作 SQL 语句执行
- 性能:可复用执行计划,减少数据库资源消耗
5.1. 各类框架的实现方式
技术栈 | 示例方法 |
---|---|
Java EE | PreparedStatement() |
Hibernate | createQuery() |
PHP (PDO) | prepare() + bindParam() |
.NET | SqlCommand() / OleDbCommand() |
5.2. 参数绑定的额外优势
- 复用执行计划:相同 SQL 模板即使参数不同,也可复用已缓存的执行计划,显著提升性能
- 防 DoS 攻击:防止攻击者通过构造大量不同 SQL 语句耗尽数据库缓存
示例攻击代码(模拟 DoS):
algorithm DatabaseCacheDoSBomb():
i <- 0
while true:
sql <- "select " + i + " from dual"
Database query sql
i <- i + 1
6. 其他安全建议与最佳实践
除了参数绑定外,还需结合以下措施增强安全性:
6.1. 输入校验与转义
✅ 对所有输入做校验:
- 数值类型:仅允许数字,限制范围
- 字符类型:限制字符集(如仅允许字母数字)
- 字段长度:限制最大输入长度
⚠️ 不要信任任何用户输入。
6.2. 存储过程(Stored Procedures)
使用存储过程可以将 SQL 逻辑封装在数据库中,并通过参数传递输入值,避免拼接 SQL。
⚠️ 注意:如果存储过程内部使用字符串拼接构建动态 SQL,则仍存在风险。
6.3. 最小权限原则(Least Privilege)
应用程序使用的数据库账户应仅具备完成任务所需的最小权限。
✅ 建议:
- 不使用 DBA 或 Schema Owner 权限账户
- 对高权限操作使用专用账户
6.4. Web 应用防火墙(WAF)
对于无法修改源码或难以修复的应用,可以使用 Web 应用防火墙(WAF)作为最后一道防线。
WAF 的作用包括:
- 校验输入参数
- 检测 SQL 注入模式
- 阻止自动化请求
- 防止敏感信息泄露(如 SSN、信用卡号)
常见开源 WAF:
- ModSecurity
- Naxsi
- Shadow Daemon
云服务如 CDN 或安全平台也常集成 WAF 功能。
⚠️ 注意:配置不当可能导致误报,影响正常业务。
7. 总结
了解不良编码可能带来的安全漏洞,是构建安全应用的关键。SQL 注入虽然常见且易于修复,但仍广泛存在。通过参数绑定、输入校验、最小权限、WAF 等手段,可以有效防范此类攻击。
在本文中,我们详细介绍了 SQL 注入的原理、分类、攻击方式以及防御策略。希望这些内容能帮助你写出更安全、更健壮的应用程序。