系列文章: Bảo mật
  1. 1 什么是恶意软件?分类、特征及预防方法
  2. 2 什么是DDoS?识别迹象、应对方法与有效防御指南
  3. 3 什么是网络钓鱼?识别与防范在线欺诈
  4. 4 什么是DNS Sinkhole?DNS Sinkhole技术的应用与使用方法
  5. 5 什么是OAuth 2.0?授权访问与谷歌登录原理
  6. 6 什么是木马病毒?关于Trojan恶意软件的基本知识
  7. 7 Zero Trust 是什么?'永不信任,始终验证'安全模型
  8. 8 VPN是什么?虚拟专用网络与WireGuard、OpenVPN协议
  9. 9 MFA 是什么?多因素认证与 2FA 对比详解
  10. 10 什么是防火墙?在网络安全中的角色和功能
  11. 11 什么是SQL注入?数据库攻击与防护
✦ 快速摘要
SQL注入(SQLi)是OWASP A03:2021漏洞,允许攻击者将恶意SQL语句注入应用程序查询,从而访问或删除整个数据库。了解攻击机制、示例载荷及使用预处理语句的防护方法。
这篇文章怎么样?

SQL注入是最危险的Web安全漏洞,存在超过25年,但仍在OWASP 2021年十大漏洞列表中名列前茅。搜索框或登录表单中输入的一个单引号('),就可能让攻击者访问您应用程序的整个数据库——无需密码,无需特殊账户。

什么是SQL注入?OWASP A03:2021

**SQL注入(SQLi)**是一种攻击技术,攻击者将恶意SQL语句注入Web应用程序的输入参数。当应用程序处理不当时,数据库引擎会将这些语句作为查询的合法部分执行——导致数据泄露、身份验证绕过或数据破坏。

OWASP 2021年十大Web应用安全风险中,SQL注入属于A03:注入类别——这是最普遍、最严重的安全风险之一。根据OWASP数据,注入漏洞(包括SQLi、LDAP注入、OS命令注入)在94%的被测应用中被发现,发生率为19%。

为什么25年后SQLi仍然盛行?

SQL注入最早记录于20世纪90年代末。25年后,自然会有这样的问题:为什么这个漏洞还没有被完全消除?

**技术原因:**许多应用程序——尤其是遗留系统——在构建SQL查询时仍然使用直接字符串拼接。这对于初学者来说是自然的写法,如果没有代码审查或SAST(静态应用安全测试),很容易通过审查。

**人为因素:**截止日期的压力导致许多团队跳过最佳实践。此外,许多继承的代码库是在预处理语句成为行业标准之前编写的。

**广泛的攻击面:**应用程序从用户接收数据(表单、URL参数、Cookie、HTTP头)并将其传入SQL查询的任何地方,都是潜在的攻击点。

攻击机制:从输入到数据库

要理解SQLi,需要了解数据从用户输入到数据库执行查询的整个流程。

易受攻击应用中的数据流

  1. 用户在表单中输入数据(用户名、密码、搜索词...)
  2. 应用程序将输入直接拼接到SQL字符串中
  3. SQL字符串被发送到数据库引擎
  4. 数据库执行所有SQL——包括攻击者注入的部分

以下是经典的易受攻击PHP代码示例:

php
1// VULNERABLE — 不要这样做
2$username = $_POST['username'];
3$password = $_POST['password'];
4$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";

正常情况下,如果用户输入alicesecret123,查询将是:

SQL
1SELECT * FROM users WHERE username='alice' AND password='secret123'

这是一个有效且无害的查询。但当攻击者在用户名字段输入admin'--时会发生什么?

攻击载荷与影响

当攻击者输入admin'--作为用户名(密码输入任意内容)时:

SQL
1-- 被注入后的查询:
2SELECT * FROM users WHERE username='admin'--' AND password='anything'
3-- 之后的内容被注释掉 → 绕过密码验证

SQL中--是注释符号(MySQL中也可以用#)。整个AND password='anything'部分被忽略。数据库只检查用户名,如果admin账户存在,攻击者无需知道密码即可成功登录。

成功攻击的后果

根据应用程序使用的数据库用户的配置和权限,攻击者可以:

  • **读取敏感数据:**整个用户表、信用卡信息、医疗记录
  • **绕过身份验证:**以任何账户(包括管理员)登录
  • **修改/删除数据:**无限制的UPDATE或DELETE
  • **转储整个架构:**了解数据库结构以规划进一步攻击
  • **执行操作系统命令:**在MySQL上使用INTO OUTFILE,在SQL Server上使用xp_cmdshell

SQL注入的类型

并非所有SQLi都以相同的机制工作。攻击者根据应用程序处理和显示结果的方式使用不同的变体。

经典(带内)SQLi

这是最常见、最直接的形式。恶意SQL语句的结果直接在应用程序响应中返回——与原始请求使用相同的通道。

**基于错误的SQLi:**应用程序显示数据库的详细错误消息。攻击者利用错误消息中的信息了解数据库结构、版本和表名。

**基于UNION的SQLi:**攻击者使用UNION运算符将另一个SELECT的结果附加到原始结果中。这是从其他表转储数据最常见的方式。

盲布尔型SQLi

当应用程序不直接显示查询结果或错误消息时,攻击者仍然可以通过观察条件为TRUE还是FALSE时响应的差异来利用它。

例如:攻击者添加条件AND 1=1(TRUE)与AND 1=2(FALSE),并观察页面是否显示正常结果或为空。从那里,可以通过询问是/否问题逐位推断信息。

这个过程很慢,但可以用sqlmap等工具完全自动化。

盲时间型SQLi

类似于布尔型,但攻击者不是观察响应内容,而是测量服务器返回响应的时间。通过使用SLEEP()(MySQL)、WAITFOR DELAY(SQL Server)或pg_sleep()(PostgreSQL)函数,攻击者可以根据延迟推断信息。

例如:IF(1=1, SLEEP(5), 0)——如果服务器需要5秒才能响应,则条件为TRUE。

带外SQLi

最罕见的攻击类型,取决于数据库服务器创建出站网络连接(DNS查询、HTTP请求)的能力。攻击者不需要直接读取响应;相反,数据通过另一个通道发送出去(例如,向攻击者控制的域发送DNS查询)。

示例载荷(教育用途)

**重要提示:**以下载荷仅用于教育目的和安全意识。使用这些技术攻击您没有权限访问的系统是违法行为。只能在您控制的测试/实验室环境中应用。

SQL
 1-- Login bypass
 2' OR '1'='1
 3' OR 1=1--
 4admin'--
 5
 6-- Data dump (UNION-based)
 7' UNION SELECT username, password, NULL FROM users--
 8
 9-- Database version
10' UNION SELECT @@version, NULL, NULL--
11
12-- Time-based blind (MySQL)
13'; SELECT SLEEP(5)--
14
15-- Drop table (destructive)
16'; DROP TABLE users--

每个载荷的解释

' OR '1'='1 — 用单引号关闭当前字符串,添加一个始终为真的条件。结果:WHERE子句始终为TRUE → 返回所有行。

admin'-- — 在用户名后关闭字符串,使用--注释掉查询的其余部分(通常是密码检查)。结果:绕过密码身份验证。

UNION SELECT — 将另一个SELECT附加到原始结果,以读取其他表中的数据(必须匹配列数和数据类型)。

SLEEP(5) — 用于时间型盲SQLi:通过测量延迟来确认漏洞存在。

DROP TABLE — 破坏性数据攻击。在实践中,许多数据库用户没有DROP权限——这就是最小权限原则重要的原因。

预防代码:预处理语句

预处理语句(也称为参数化查询)是防止SQL注入最有效、最可靠的方法。预处理语句不是将输入拼接到SQL中,而是将SQL结构数据完全分离。

工作原理:

  1. 应用程序向数据库发送SQL模板(带?:name占位符)
  2. 数据库编译解析SQL模板——此时查询结构已经固定
  3. 应用程序单独发送实际数据
  4. 数据库使用绑定到占位符的数据执行查询——数据无法改变查询结构

以下是在常用语言中的正确实现:

php
1// PHP PDO — SAFE
2$pdo = new PDO($dsn, $user, $pass);
3$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
4$stmt->execute([$username, $password_hash]);
5$user = $stmt->fetch();
Python
1# Python psycopg2 — SAFE
2import psycopg2
3conn = psycopg2.connect(dsn)
4cur = conn.cursor()
5cur.execute(
6    "SELECT * FROM users WHERE username = %s AND password = %s",
7    (username, password_hash)
8)
9user = cur.fetchone()
Java
1// Java JDBC — SAFE
2String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
3PreparedStatement stmt = conn.prepareStatement(sql);
4stmt.setString(1, username);
5stmt.setString(2, passwordHash);
6ResultSet rs = stmt.executeQuery();

为什么预处理语句是安全的?

当攻击者在用户名字段输入admin'--时,使用预处理语句:

  • 数据库已经知道查询结构:WHERE username = ? AND password = ?
  • admin'--作为字符串字面量传入,而不是SQL代码
  • 数据库将其视为普通字符串——包括'--
  • 不会发生注入

ORM和SQLAlchemy——安全还是不安全?

ORM(对象关系映射)如SQLAlchemy、Hibernate、ActiveRecord...提供了SQL之上的抽象层。一个常见问题:"我使用ORM了,还需要担心SQLi吗?"

答案:是的,仍然需要担心——如果您在ORM中使用原始SQL。

Python
 1# SQLAlchemy — SAFE (ORM query)
 2user = session.query(User).filter(User.username == username).first()
 3
 4# SQLAlchemy — VULNERABLE (raw f-string)
 5result = session.execute(f"SELECT * FROM users WHERE username = '{username}'")
 6
 7# SQLAlchemy — SAFE (raw SQL with bindparam)
 8from sqlalchemy import text
 9result = session.execute(
10    text("SELECT * FROM users WHERE username = :username"),
11    {"username": username}
12)

分析

**ORM查询构建器(安全):**当您使用.filter(User.username == username)时,SQLAlchemy会自动在后台创建参数化查询。不会发生字符串拼接。

**原始f-string(不安全):**这是开发人员想要编写自定义SQL时最常见的错误。f-string将用户名直接拼接到SQL字符串中——这正是SQLi利用的模式。

**text()与命名参数(安全):**当需要原始SQL时,始终使用SQLAlchemy的text():param_name占位符,并通过字典传递值。SQLAlchemy将自动使用参数化查询。

关于存储过程的注意事项

存储过程不会自动防止SQLi。如果存储过程内部使用动态SQL(EXEC、sp_executesql)与字符串拼接,它仍然存在漏洞。请始终检查存储过程内部的代码。

WAF、输入验证及其为何不足够

Web应用防火墙(WAF)

WAF通过分析HTTP请求并阻止包含已知SQLi模式的请求(单引号、UNIONSELECTDROP等关键字)来工作。这听起来很有效,但有许多绕过技术:

编码绕过:%27'的URL编码。许多WAF在检查前不能正确解码。

注释插入:UN/**/ION——关键字中间的SQL注释。MySQL和某些其他数据库接受这种语法。

大小写变化:SeLeCtuNiOnsElEcT——使用区分大小写匹配的WAF会被愚弄。

双重编码:%2527 → 解码为%27 → 解码为'

**空白替代:**制表符、换行符、回车符可以替代SQL中的空格。

输入验证——双刃剑

一些团队尝试通过删除或转义特殊字符(如'";)来防止SQLi。问题在于:

  • 破坏合法数据:O'BrienD'Souza等用户名完全合法,但会被阻止。
  • **不够全面:**有数十种方法可以对SQLi载荷进行编码和混淆。
  • **虚假的安全感:**开发人员认为已经安全,而实际上漏洞仍然存在。

深度防御:正确策略

有效的SQLi防护需要多层保护:

  1. **预处理语句(必须):**这是基本且不可替代的措施。没有预处理语句,任何其他保护层都不够有效。

  2. **最小权限数据库用户:**应用程序使用的数据库账户应只具备最小必要权限(在特定表上的SELECT、INSERT、UPDATE)。永远不要为应用程序使用root/sa/admin账户。

  3. **WAF(补充):**检测并阻止已知攻击,减少日志中的噪音。但不是主要解决方案。

  4. **SAST/DAST:**静态应用安全测试,在开发过程中检测易受攻击的代码模式。

  5. **正确的错误处理:**不要向用户显示堆栈跟踪或详细的数据库错误消息。在服务器端记录错误,向客户端返回通用消息。

  6. **监控和告警:**检测异常模式(多次尝试包含'的输入、异常响应时间)以便及时响应。

OWASP参考资料与著名CVE

OWASP SQL注入防护备忘单

OWASP在OWASP SQL注入防护备忘单提供了详细的SQL注入防护指南。该文档涵盖:

  • 每种语言的预处理语句API列表
  • 存储过程——何时安全,何时不安全
  • 转义——仅在无法使用预处理语句时作为最后手段
  • 输入验证——如何正确执行
  • 最小权限建议

与SQL注入相关的著名CVE

CVE-2011-4505 — Joomla SQL注入: Joomla CMS中的SQLi漏洞影响了全球超过150万个网站。攻击者可以通过URL参数执行未经身份验证的SQL注入,读取包括管理员登录凭据在内的整个数据库。这是CMS历史上影响最广泛的CVE之一。

雅虎2012年数据泄露 — SQL注入: 2012年,黑客组织D33Ds Company宣布通过SQL注入从雅虎语音(前身为Associated Content)窃取了45万条登录凭据(明文用户名和密码)。这次泄露不仅影响了雅虎,还影响了其他服务,因为许多用户在多个账户中使用相同密码。

历史攻击的教训:

  • SQLi不仅针对小型应用程序——大公司和流行平台也可能受到影响。
  • 库/CMS中的单个SQLi漏洞可能影响运行该平台的数百万个网站。
  • 被盗数据通常被出售或公开披露——声誉损失通常大于直接经济损失。

总结:SQLi防护清单

在部署任何与数据库交互的功能之前,请检查:

  • 所有SQL查询都使用预处理语句/参数化查询
  • SQL代码中没有f-string/字符串拼接
  • ORM原始查询(如有)使用bindparam,而非f-string
  • 数据库用户只有最小必要权限
  • 错误消息不暴露数据库信息/堆栈跟踪
  • WAF已配置并正常运行(深度防御)
  • 代码已通过SAST扫描以检测注入模式

SQL注入是完全可以预防的。与许多其他复杂的安全漏洞不同,SQLi的解决方案简单明了:始终使用预处理语句,永远不要从用户输入中拼接SQL字符串。从一开始就养成正确的编码习惯,将完全消除这种风险。

什么是XSS?跨站脚本与防护

什么是防火墙?