SQL 注入 · 网络安全入门
在登录框里输入几个字,怎么就能拿到管理员权限?
课程目录为什么需要它
二十多年来,SQL 注入一直是危害最大的网站漏洞之一,现在的新代码里也还能见到。它源于一个习惯:把用户输入直接拼进数据库查询的字符串里。看懂了问题,修起来很简单。
直觉理解
想象一封套用信:「亲爱的 ____,您的订单已发货。」如果有人把名字写成 Bob。另外,全部退款。,粗心的职员会把整句当成指令来执行。
把用户输入直接拼进 SQL,就等于让用户来写你程序的一部分。一个引号 ' 会提前结束你的字符串,后面的内容就变成了 SQL。' OR '1'='1 把密码检查变成了「或者为真」。
参数化查询把 SQL 和数值分开发送:? 标出值的位置,数据库只把它当数据,永远不会当代码执行。再多的引号也改变不了这条查询。
(为了让例子简短,这里的密码用明文保存。第 3 课已经讲了怎样用哈希存。)
定义
SQL 注入
An attack where input placed into a SQL string changes the meaning of the query, for example to skip a password check or read other tables.一种攻击:放进 SQL 字符串的输入改变了查询的含义,比如跳过密码检查或读取别的表。
参数化查询
A query with placeholders such as `?`, whose values are passed separately: `db.execute(sql, (a, b))`. Values can never become SQL code.带占位符(比如 `?`)的查询,值单独传入:`db.execute(sql, (a, b))`。值永远不会变成 SQL 代码。
示例
能被骗过的登录
动手试试函数会打印它拼出来的 SQL,这样你能看到数据库究竟收到了什么。
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (name TEXT, password TEXT)")
db.execute("INSERT INTO users VALUES ('admin', 's3cret!')")
def login(name, password):
sql = "SELECT name FROM users WHERE name = '" + name + "' AND password = '" + password + "'"
print(sql)
return db.execute(sql).fetchone() is not None
print(login("admin", "guess"))
print(login("admin", "' OR '1'='1"))- L2内存里的数据库,只有一个用户。
- L7输入被直接拼进 SQL 文本。
- L11密码错误:查不到行,所以是 False。
- L12引号把字符串提前结束,
OR '1'='1'永远为真:不用密码就登录了。
修复:用占位符
动手试试同一个数据库,同样的攻击。? 占位符让输入始终只是数据。
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (name TEXT, password TEXT)")
db.execute("INSERT INTO users VALUES ('admin', 's3cret!')")
def login(name, password):
sql = "SELECT name FROM users WHERE name = ? AND password = ?"
return db.execute(sql, (name, password)).fetchone() is not None
print(login("admin", "' OR '1'='1"))
print(login("admin", "s3cret!"))- L7SQL 本身永远不变,
?只是标出位置。 - L8值作为一个元组单独传过去。
- L10攻击字符串现在只是一个对不上的怪密码。
False - L11真正的密码照样能登录。
True
常见错误
自己手动转义引号
自己把 ' 替换成 '' 总会漏掉情况(别的引号、编码、不带引号的数字)。占位符更简单,而且永远正确。
表名列名用占位符
? 只能用于值,不能用于表名或列名。如果要让用户选列(比如排序),就用允许清单检查,比如 {"name", "price"}。
小测验
1. 这段代码会输出什么?
name = "x' OR 1=1 --"
sql = "SELECT * FROM users WHERE name = '" + name + "'"
print(sql)查看答案
答案:A
输入里的引号把字符串结束了。-- 开始一段 SQL 注释,把剩下的那个引号藏了起来。
2. 哪一行不会被 SQL 注入?
查看答案
答案:C
f-string 和 + 都会把输入放进 SQL 文本。只有占位符能把它分开。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出数据库看到了什么?
准确写出这段程序的输出。
- 改需求修好这个查询
find_user(name)用 f-string 拼 SQL,所以最下面的攻击能查出用户。把查询改成用?占位符,使得:find_user("bob")仍然返回('bob', 'bob@example.com')- 攻击返回
None
- 从零写安全的搜索
写一个函数
search(term),返回名字里包含term的商品名字列表,按名字排序。用LIKE加占位符:模式是"%" + term + "%",作为值传进去,而不是拼进 SQL。像
search("' OR 1=1 --")这样的攻击必须返回[]。
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| SQL injection | SQL 注入 | Input that changes the meaning of a SQL query because it was pasted into the SQL text. |
| Placeholder (?) | 占位符(?) | A marker in a SQL query where a value is filled in safely by the database driver. |
| Allow-list | 允许清单 | A list of the only values that are accepted; everything else is rejected. |