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)

应用不返回任何错误或结果信息时,攻击者通过布尔值或时间延迟等方式进行“猜解”。

  • 布尔盲注:通过判断真假值(如页面是否返回特定内容)来获取信息。
  • 时间盲注:通过 IFSLEEP 等函数控制响应时间,从而推断信息。

缓解建议

  • 不返回具体错误信息
  • 固定响应时间,避免时间差异被利用
  • 限制高频请求频率

4. 高级 SQL 注入混淆技术

有些应用通过检测输入中是否包含 SQL 关键字(如 selectupdatedelete)来防御注入。但这远远不够。

攻击者会使用各种混淆技术绕过检测,例如:

  • 大小写混合(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]

参数绑定的工作流程如下:

  1. prepare:发送 SQL 模板给数据库,数据库解析并缓存执行计划
  2. bind:绑定参数值
  3. execute:执行查询
  4. 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 注入的原理、分类、攻击方式以及防御策略。希望这些内容能帮助你写出更安全、更健壮的应用程序。


原始标题:What Is SQL Injection?