SQL 注入攻击(SQL 注入)的本质只有一句话:程序把"用户输入的数据"和"要执行的 SQL 语句"拼成了一串文字,于是输入里的引号可以提前把字符串关掉,后面跟的内容就被数据库当成了命令来执行——它是本章里唯一一个"输入本身就是代码"的漏洞,也是唯一一个可以靠一个标准动作(参数化)一次性根除的漏洞。
4-3 讲的是"没有补丁可打"的难处,这一节回到最可防的那一类:SQL 注入属于把整类问题根除得最干净的一种漏洞。
它和 第二章 的整条线是连着的:第二章讲的攻击目标是"通道"(窃听、篡改、冒充),这一节讲的目标是"数据"——而 SQL 注入是"直接对着数据库说话"的最短路径,所以它长期排在漏洞榜的第一位。
一、先说清一句话:数据变成了代码
正常的程序逻辑是"两部分分开"的:
- 代码:
SELECT * FROM user WHERE name = '?'——由开发者写死,永远不会变; - 数据:
'alice'——由用户提供,内容不可信。
SQL 注入之所以能成立,是因为很多程序偷懒,把这两部分用字符串拼接合成了一句话:
# 拼接式(错):数据和代码混在一根字符串里
sql = "SELECT * FROM user WHERE name = '" + user_input + "'"
这样一来,用户在输入框里敲的内容,直接变成了 SQL 语句的一部分。 他只要在输入里放一个单引号,就能把开发者写的那个字符串提前"关掉",然后在后面接上自己想执行的 SQL。
所以 SQL 注入不是"破译了数据库",而是"数据库太听话"——数据库忠实地执行了它收到的那条语句,只不过那条语句是攻击者写的。
二、为什么会成立:一个引号的连锁反应
把拼接的过程一步步摊开,就非常清楚了。假设用户输入是:
x' OR '1'='1
程序拼出来的语句是:
SELECT id, name, role FROM user WHERE name = 'x' OR '1'='1'
看这个结果里的引号:开发者写的那对引号,被用户输入里的那个单引号提前闭合了,于是:
name = 'x'——这是一个正常的条件,但没人叫 x,所以它是假的;OR '1'='1'——这是攻击者新加的条件,而它永远为真。
一个"用户不存在"的查询,变成了"返回所有用户"。 这就是 SQL 注入的完整原理——它不复杂,只是很有效。
数据库本身并没有做错任何事:它执行的就是"名字是 x,或者 1 等于 1 的那些用户",这完全符合语法。 问题出在拼接这个动作上,让"数据"获得了"代码"的身份。
三、动手 Demo:真跑一个数据库,看两种写法的差别
下面这段脚本用 Python 自带的 sqlite3 建了一个内存数据库(不是模拟,是真的数据库),库里 4 个用户,然后用两种写法执行同一个查询:
import sqlite3
con = sqlite3.connect(":memory:") # 内存数据库,关机即消失
cur = con.cursor()
cur.execute("CREATE TABLE user(id INTEGER, name TEXT, role TEXT)")
cur.executemany("INSERT INTO user VALUES(?,?,?)",
[(1, "alice", "admin"), (2, "bob", "user"),
(3, "carol", "user"), (4, "dave", "user")])
evil = "x' OR '1'='1"
q1 = "SELECT id, name, role FROM user WHERE name = '%s'" % evil # 拼接
q2 = "SELECT id, name, role FROM user WHERE name = ?" # 参数化
print("拼接取到", len(cur.execute(q1).fetchall()), "行")
cur.execute(q2, (evil,))
print("参数化取到", len(cur.execute(q2, (evil,)).fetchall()), "行")
实跑输出:
库里一共 4 行:alice(admin) / bob / carol / dave
一、用户输入:x' OR '1'='1
拼接式实际执行的 SQL:
SELECT id, name, role FROM user WHERE name = 'x' OR '1'='1'
取到 1 / alice / admin
取到 2 / bob / user
取到 3 / carol / user
取到 4 / dave / user
→ 返回 4 行(本应 0 行)
参数化实际执行的 SQL:
SELECT id, name, role FROM user WHERE name = ? 参数 = x' OR '1'='1
→ 返回 0 行
二、换成带注释符的输入:x' OR 1=1 --
SELECT id, name, role FROM user WHERE name = 'x' OR 1=1 --'
→ 返回 4 行(-- 把后面的引号全注释掉了)
→ 结论:【拼接让数据变成了代码,参数化把两者彻底分开】。
2026 上半年 CWE-89(SQL 注入)仍是被报漏洞第一名(1,116 个)。
读这个输出:
- 同一个输入,同一个数据库,结果一个是 4 行、一个是 0 行——差别只在于"数据有没有被当代码处理"。 这就是"根除"的含义:不是把危险字符过滤掉,而是从结构上让数据永远不可能变成代码。
- 第二部分展示了一个更省事的写法:
--是 SQL 的注释符,攻击者连后面开发者写的那个收尾引号都不用管了,直接注释掉就行。这说明"过滤引号"这种防御是脆弱的——你堵住一种写法,还有别的写法。 - 正确的做法叫"参数化查询(预编译语句)":程序先把 SQL 语句的"骨架"发给数据库编译好,再把数据作为一个独立的参数传进去。数据永远待在"参数"这个位置上,无论它长什么样,都不会改变语句的结构。 这是唯一一个"一次做对、永久有效"的动作。
四、⚠️ 2026 现状
- 【SQL 注入仍然排第一,但占比在明显下降】。 对 2026 年上半年 18,464 个高危/极危漏洞的统计中,SQL 注入(CWE-89)仍是数量最多的单一类型,共 1,116 个;但 2025 年同一口径下的数字是 3,270 个(占 15.43%)——也就是说,绝对数和占比都在大幅回落。 这个趋势的含义是:这个漏洞的解法早就有了(参数化),剩下的主要是"老代码里还留着拼接式写法"。
- 【OWASP 榜单上的位置在下移】。 2021 版里"注入类"排第 3,2025 版降到第 5(OWASP Top 10:2025)。它没有消失,但已经从"最常见的问题"退到"仍然要防的问题"——这也符合"根除整类漏洞"的工程趋势(4-11)。
- 【新的注入面转移到了 API 和结构化查询上】。 SQL 注入在传统网页表单上被压下去之后,同类问题开始出现在别的地方:API 接口的参数、NoSQL 数据库的查询对象、ORM 框架里没法参数化的那些"原始 SQL"片段。2026 年 WAF 市场的一个重要变化就是"API 攻击激增"(4-10)。结论:参数化这条路要一直走到"每一个拿用户输入拼查询的地方",包括那些看起来不像表单的地方。
- 【插件生态是注入漏洞的重灾区】。 2026 年第一季度,WordPress 插件新披露的漏洞有 2,738 个;在一份单周 358 条的清单里,SQL 注入 35 条(排第三)(4-12)。这些插件里的大量代码是多年前写的,用的还是拼接式——这也是"用现成的第三方代码,等于把它当年的安全水平一起继承过来"的直接例子。
- 【WAF 能拖时间,但不能替代修复】。 一条拦截 SQL 注入特征的规则可以挡住绝大多数自动扫描器和现成的利用工具(4-10),但它挡不住为你的系统量身定制的绕过。而参数化是"漏洞不存在",WAF 是"漏洞被绕开的难度变高了"——两者的性质不同,不能互相顶替。
30 秒实验
今天做两个动作,各 15 秒:
- 打开你常用的任何一个有"搜索"或"登录"的网页,在输入框里敲一个孤零零的单引号(就是
'),然后提交。如果页面报错、显示出数据库相关的错误信息(比如SQL syntax、You have an error in your SQL),那说明两件事:① 这里很可能是拼接式写法;② 错误信息被直接暴露给了用户——后者本身就是一个信息泄露漏洞(正确做法是显示一句"输入有误",把细节留在服务端日志里)。 - 如果你手边有任何和数据库打交道的代码,今天只搜一个东西:搜
+或%出现在 SQL 字符串附近的写法(比如"SELECT ... " + name、"... '%s'" % input)。找到一个就改造一个为参数化写法。这一步的意义是:SQL 注入的修复不需要"筛查全部代码"这种大工程,它是一件可以小步持续做的事。
这一节要带走的一句是:SQL 注入的原理是"数据被当成了代码",所以它的解法也只有一条——让数据和代码永远分开放(参数化),而不是靠"过滤掉那些危险字符"。
常见误区
- 错误:把单引号过滤掉,就不会有 SQL 注入了。——纠正:过滤危险字符是"猜攻击者的写法",而攻击者的写法是无穷的。这一节的 Demo 里第二部分就展示了一种绕过思路:用
--注释符,根本不需要配对引号。除此之外还有编码、大小写混写、利用数据库特有的语法等许多变体。 唯一可靠的办法是参数化查询(预编译语句)——它的原理不是"让危险输入失效",而是"从结构上让数据不可能变成代码"。 过滤可以做一层(作为纵深防御),但它绝不能是唯一的防线。 - 错误:我的数据库设了强口令、只监听内网,注入打不进来。——纠正:SQL 注入走的不是"网络通道",而是"应用程序的正常入口"。攻击者不需要连上你的数据库——他是通过你的网页表单、API 参数,让【你的程序】带着【你的数据库账号】去执行他想要的语句。所以数据库的网络隔离、强口令对 SQL 注入基本无效——因为执行语句的是合法的应用程序自己。真正有效的是:应用层不拼 SQL(参数化)、数据库账号最小权限(只给应用需要的表和操作)。
- 错误:用 ORM 框架就自动免疫 SQL 注入了。——纠正:ORM 的常规用法是安全的(它内部就是参数化),但它是可以"绕过去"的。几乎每个 ORM 都提供"执行原生 SQL"的接口(比如各种
raw()、execute()方法),而这些接口往往就是把用户输入拼进去的地方。2026 年的观测也印证了这一点:注入面正在向"框架里没法参数化的原始 SQL 片段"转移。所以正确的说法是:ORM 减少了出错面,但没有取消"知道自己在拼什么"这个责任。 - 错误:这是开发者的问题,跟我们使用者没关系。——纠正:SQL 注入是少数"用户侧也能立刻发现"的漏洞。本节 30 秒实验里的那个单引号测试,任何人都能做——你不需要看懂代码,只要看它有没有把数据库错误信息吐给你。对采购和使用者来说,这提供了一条很实用的验收动作:在选型或验收时,随手在几个输入框里敲单引号,看它报不报数据库错误。这件事的第二层意义是:一个连"错误信息不要直接暴露给用户"都没做的系统,通常在其他安全细节上也不会有多讲究。
自测三问(点开对答案)
1. SQL 注入的原理,用一句话怎么说清楚? —— **程序把"用户输入的数据"和"要执行的 SQL 代码"拼成了一根字符串,于是输入里的引号可以提前把字符串关掉,后面跟的内容被数据库当成了命令来执行**。**关键在"拼接"这个动作**:**它让数据和代码失去了边界**。**这也是为什么 SQL 注入被称为"注入"——数据被注入到了代码的位置上**。**理解了这一点,解法也就自然出来了:只要保证数据和代码从生成到执行的全过程都不混在一根字符串里(参数化查询),这个漏洞就不可能出现。** 2. 为什么说"过滤引号"不能算修好了 SQL 注入? —— **因为过滤是在"猜攻击者的写法",而写法是无穷的**。**这一节的 Demo 里,攻击者只要用 `--` 把后面的引号注释掉,就完全不需要配对引号**;**此外还有各种编码、数据库特有语法等变体**。**更根本的问题是:过滤针对的是一种"输入形式",而注入针对的是"语句结构"**——**只要结构上还有"数据被拼进代码"的位置,就总会有人找到一种过滤没覆盖到的写法。** **参数化之所以有效,是因为它不是在"检查数据长什么样",而是在"让数据根本没有机会变成代码"**——**这是结构上的修复,而不是形式上的拦截。** 3. "SQL 注入仍排第一,但占比在明显下降"——这两半分别说明了什么? —— **"仍排第一"说明它没有消失,是存量问题**:**大量多年前写的代码还在用拼接式**(尤其第三方插件里,2026 年单周 358 条插件漏洞中 SQL 注入就有 35 条),**而这些问题会随着代码存活一直被报出来**。**"占比在下降"说明解法确实有效、而且在被推广**:**从 2025 年的 3,270 个(15.43%)降到 2026 年上半年的 1,116 个**——**新写的代码大多已经参数化了**。**两半合起来给出的行动指引是:新代码必须参数化(这是"不再增加"),老代码要按风险排优先级去改(这是"逐步减少")**,**而后者正好可以用 4-13 的排序办法来决定先改哪一段。**- ☐ 下一节(4-5 跨越多个网站的攻击)
相关笔记
- 存在无法防范的攻击吗 —— 上一篇:没有补丁时的处境
- 跨越多个网站的攻击 —— 下一篇:跨网站式脚本攻击(XSS)
- 信息安全的三要素 —— 机密性、完整性、可用性被打穿的例子
- 当心那些使用方便的工具中存在的漏洞 —— 插件是注入漏洞的重灾区
- 🛡️ 网络与信息安全 · 索引 —— 全课路线图