SQL 注入的成因也是「拼接」:用户输入被拼进了 SQL 语句,于是他能改掉语句的结构。
' OR '1'='1' -- 把 WHERE 变成永真,整张表都出来了。参数化查询能根治,过滤关键字只能算补丁。
上一节是数据变成 HTML,这一节是数据变成 SQL。同一个病,换个宿主。
一、一句话把条件变成永真
朴素版的登录是这么写的:SELECT * FROM users WHERE name = '%s' AND pw = '%s',
两个参数直接拼进去。
正常输入没问题。
可如果 name 填 ' OR '1'='1' --,
拼出来的语句就变成 WHERE name = '' OR '1'='1' --' AND pw = 'x'。
-- 是 SQL 的注释符,它把后面整段注释掉了。
于是条件只剩 name = '' OR '1'='1'——永真语句,整张表都被选出来。
Demo 里这一条返回了 3 行(三个用户全在里面),而正常登录只返回 1 行。
绕过登录只是最小的用法,厉害的还在后面。
二、从「绕过」到「拖库」
UNION 能把别的表接在后面。
Demo 里 ' UNION SELECT 99,'injected' -- 让结果里多出一行凭空造的数据——
这说明攻击者可以控制返回内容。
把它换成 UNION SELECT username, password FROM users,
整张用户表的账号密码就跟着正常结果一起返回了。这叫拖库。
更进一步还有「盲注」:页面不回显数据,但会因为你构造的条件真假而表现不同(比如登录成功/失败)。
攻击者一个字符一个字符地问,照样能把数据问出来——只是慢。
所以「不回显就安全」是错的。只要输入能改 SQL 结构,就一定能读出数据。
三、参数化才是根治
修正版只改一处:SQL 里写 WHERE name = ? AND pw = ?,参数单独传给驱动。
同一个 ' OR '1'='1' -- 进去,返回 0 行。
关键不在「过滤了引号」,在参数根本不参与拼 SQL。
驱动把参数当纯数据发送,语句的结构在执行前就定死了。
这也解释了为什么各种「过滤关键字」的方案都不彻底:编码变化、注释、大小写、不同数据库的方言,
总能绕过去。
有一条经验值得记:看到字符串拼 SQL 就当成漏洞处理,不管那个变量看起来多可信。
今天不可信的是用户输入,明天可能是从别的接口拿来的值。
四、动手 Demo:四组输入,拼接与参数化对比
内存里建一张三个用户的表,写两个登录函数:一个拼字符串,一个用参数化。 各喂四组输入:正常、密码错、永真注入、UNION 注入。
sql = (u"SELECT id, name FROM users WHERE name = '%s' "
u"AND pw = '%s'" % (name, pw))
rows = list(con.execute(sql)) # 拼接:结构可被改
sql = u'SELECT id, name FROM users WHERE name = ? AND pw = ?'
rows = list(con.execute(sql, (name, pw))) # 参数化:结构定死
实跑输出:
--------------------------------------------------------------------
SQL 注入:一句话把 WHERE 变成永真
--------------------------------------------------------------------
朴素版(拿字符串拼 SQL):
正常登录 1 行 → [(1, 'bozai')]
密码乱填 0 行 → []
永真注入 3 行 → [(1, 'bozai'), (2, 'guest'), (3, 'root')]
UNION 多塞一行 1 行 → [(99, 'injected')]
第三行那句拼出来的 SQL 是:
SELECT id, name FROM users WHERE name = '' OR '1'='1' --' AND pw = 'x'
看出来了吗?`' OR '1'='1' --` 里的 `--` 是 SQL 的注释符,它把后面「AND pw = 'x'」整段注释掉了。
于是条件只剩下 `name = '' OR '1'='1'`——永真。整张表都出来了。
修正版(参数化查询):
正常登录 1 行 → [(1, 'bozai')]
密码乱填 0 行 → []
永真注入 0 行 → []
UNION 多塞一行 0 行 → []
修正版的 SQL 始终是 SELECT ... WHERE name = ? AND pw = ?
参数去哪了 不作为 SQL 文本,单独传给驱动
关键不在「过滤了引号」,在**参数根本不参与拼 SQL**。驱动把参数当成纯数据发送,SQL 的结构在执行前就定死了,改不了。
拼接版四组分别返回 1/0/3/1 行。第三组是永真注入,三个用户全出来了;
第四组那 1 行是 99,'injected'——攻击者凭空造的。
参数化版四组分别返回 1/0/0/0 行。
第 3、4 组都是 0,因为那串东西被当成一个普通字符串去比对,没有用户名长这样。
Demo 还打出了永真注入拼出来的完整 SQL,一眼就能看出 -- 把后面注释掉了。
注意参数化版不是「把引号转义了」——它压根没把参数放进 SQL 文本里。
这是机制上的差别,不是过滤强度的差别。
30 秒实验
- 把
--换成#,看还能不能注释掉后面的条件(不同数据库注释符不一样)。 - 给
name传一个超长的合法字符串,看参数化版会不会报错。 - 搜一搜你手头项目里的
execute(调用,看有没有字符串拼接的。
常见误区
- 「过滤掉单引号就安全了」——编码、注释、方言都能绕过,治标不治本。
- 「页面不回显数据,注入了也没用」——盲注一个字符一个字符问也能问出来。
- 「这个参数是内部的,不会被用户改」——拼接本身就是隐患,别赌来源。
自测三问(点开对答案)
1. `' OR '1'='1' --` 为什么能绕过登录? —— `--` 注释掉了后面的密码条件,剩下的 `OR '1'='1'` 是永真。 2. 参数化查询为什么能根治? —— 参数不参与拼 SQL,语句结构在执行前就定死,改不了。 3. 什么是盲注? —— 页面不回显数据,靠「条件真假导致的表现差异」逐字符问出数据。SQL 之外,还有一种更直接的:输入被拼进了 shell 命令。
- ☐ (本节完,下一节 11.2.3 OS命令注入攻击)