SQL注入是最危险的Web安全漏洞,存在超过25年,但仍在OWASP 2021年十大漏洞列表中名列前茅。搜索框或登录表单中输入的一个单引号('),就可能让攻击者访问您应用程序的整个数据库——无需密码,无需特殊账户。
需要企业数据解决方案?
自 2019 年起,AlgoData 为企业提供数据工程、分析与 AI 解决方案。
什么是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,需要了解数据从用户输入到数据库执行查询的整个流程。
易受攻击应用中的数据流
- 用户在表单中输入数据(用户名、密码、搜索词...)
- 应用程序将输入直接拼接到SQL字符串中
- SQL字符串被发送到数据库引擎
- 数据库执行所有SQL——包括攻击者注入的部分
以下是经典的易受攻击PHP代码示例:
1// VULNERABLE — 不要这样做
2$username = $_POST['username'];
3$password = $_POST['password'];
4$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
正常情况下,如果用户输入alice和secret123,查询将是:
1SELECT * FROM users WHERE username='alice' AND password='secret123'
这是一个有效且无害的查询。但当攻击者在用户名字段输入admin'--时会发生什么?
攻击载荷与影响
当攻击者输入admin'--作为用户名(密码输入任意内容)时:
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查询)。

示例载荷(教育用途)
**重要提示:**以下载荷仅用于教育目的和安全意识。使用这些技术攻击您没有权限访问的系统是违法行为。只能在您控制的测试/实验室环境中应用。
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结构与数据完全分离。
工作原理:
- 应用程序向数据库发送SQL模板(带
?或:name占位符) - 数据库编译并解析SQL模板——此时查询结构已经固定
- 应用程序单独发送实际数据
- 数据库使用绑定到占位符的数据执行查询——数据无法改变查询结构
以下是在常用语言中的正确实现:
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();
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()
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。
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模式的请求(单引号、UNION、SELECT、DROP等关键字)来工作。这听起来很有效,但有许多绕过技术:
编码绕过:%27是'的URL编码。许多WAF在检查前不能正确解码。
注释插入:UN/**/ION——关键字中间的SQL注释。MySQL和某些其他数据库接受这种语法。
大小写变化:SeLeCt、uNiOn、sElEcT——使用区分大小写匹配的WAF会被愚弄。
双重编码:%2527 → 解码为%27 → 解码为'。
**空白替代:**制表符、换行符、回车符可以替代SQL中的空格。
输入验证——双刃剑
一些团队尝试通过删除或转义特殊字符(如'、"、;)来防止SQLi。问题在于:
- 破坏合法数据:
O'Brien、D'Souza等用户名完全合法,但会被阻止。 - **不够全面:**有数十种方法可以对SQLi载荷进行编码和混淆。
- **虚假的安全感:**开发人员认为已经安全,而实际上漏洞仍然存在。
深度防御:正确策略
有效的SQLi防护需要多层保护:
-
**预处理语句(必须):**这是基本且不可替代的措施。没有预处理语句,任何其他保护层都不够有效。
-
**最小权限数据库用户:**应用程序使用的数据库账户应只具备最小必要权限(在特定表上的SELECT、INSERT、UPDATE)。永远不要为应用程序使用root/sa/admin账户。
-
**WAF(补充):**检测并阻止已知攻击,减少日志中的噪音。但不是主要解决方案。
-
**SAST/DAST:**静态应用安全测试,在开发过程中检测易受攻击的代码模式。
-
**正确的错误处理:**不要向用户显示堆栈跟踪或详细的数据库错误消息。在服务器端记录错误,向客户端返回通用消息。
-
**监控和告警:**检测异常模式(多次尝试包含
'的输入、异常响应时间)以便及时响应。

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字符串。从一开始就养成正确的编码习惯,将完全消除这种风险。

