flawopen.com/SQL 注入/Python

Python 中的 SQL 注入

严重 CWE-89 草稿 —— 等待审核
Language: English Deutsch Español Français हिन्दी Bahasa Indonesia 日本語 한국어 Português (Brasil) Русский 简体中文
通俗解释

想象一个表单,本来只期望你填入一个像 482 这样的工单编号。SQL 注入就是当有人没有填入数字,而是输入了一段"伎俩文字",导致系统回应"把所有工单都给我看",而不是只显示 482 号工单——因为系统从未真正检查过收到的内容是不是纯粹的数字。

本页关键术语
用户可控输入
任何最终来自使用者——或攻击者——的值:表单字段、URL 参数、上传文件名、HTTP 请求头。应用程序不能假定它格式正确或是安全的。
SQL 查询
发送给数据库的命令,例如"获取这一行"、"删除这张表"。它的含义完全取决于其精确文本,这正是向其中注入额外文本会造成危险的原因。

发生了什么

SQL 注入发生在用户可控输入被直接拼接进数据库查询的文本中,而不是作为独立的值传递时。如果代码通过拼接字符串来构造查询,攻击者就可以提供能改变查询实际结构的输入——把一条"查一行"的查询变成返回所有行,甚至删除整张表的查询。

在 Python 中,这几乎总是以同一种方式出现:用 f-string、% 格式化,或者简单的 + 拼接来构造数据库调用,而不是使用数据库驱动内置的参数占位符。

真实世界的影响

2015 年,英国电信运营商 TalkTalk 遭遇数据泄露,超过 15 万名客户受到影响,起因是攻击者利用了一个通过收购继承而来的老旧网页中的 SQL 注入漏洞。英国数据保护监管机构对 TalkTalk 处以 40 万英镑罚款,认定这一失误"本可避免且属于基础性错误"。

来源:英国信息专员办公室(ICO)处罚通知,2016 年——详见下方"参考资料"。

存在漏洞 vs. 已修复

存在漏洞
# user_id 直接来自请求
def get_user(cursor, user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"
    cursor.execute(query)
    return cursor.fetchone()
已修复
# 值单独传递,从不拼接进文本
def get_user(cursor, user_id):
    query = "SELECT * FROM users WHERE id = %s"
    cursor.execute(query, (user_id,))
    return cursor.fetchone()

为什么这个修复有效

修复后的版本把查询文本和值作为两个独立的参数传给 execute()。数据库驱动也会把它们分开发送给数据库——查询的结构在值被附加之前就已经固定,因此无论值包含什么字符,都不可能被解释为 SQL 语法的一部分。f-string 做不到这一点:当 execute() 拿到查询时,值早已被烘焙进文本里,就好像它一直就是命令的一部分。

Python 特有的注意事项

execute() 内部的 % 格式化看起来像参数化,实际上不是

cursor.execute("... WHERE id = %s" % user_id) 和 f-string 一样危险。只有当值作为 execute() 自身的第二个参数传入时,占位符才会真正安全——cursor.execute("...WHERE id = %s", (user_id,))——这样做替换的是驱动本身,而不是 Python 的字符串格式化。

ORM 默认会参数化,但其"逃生通道"不会

Django 的 ORM 和 SQLAlchemy 的查询构造器在正常查询中都会自动参数化。一旦你使用 Model.objects.raw() 或 SQLAlchemy 的 text(),并用 f-string 拼出这段原生 SQL,风险就又回来了。

不同驱动的占位符语法并不统一

psycopg2(PostgreSQL)不论列类型一律使用 %ssqlite3 使用 ?。把某个驱动文档里的占位符风格照搬到另一个驱动上,会悄无声息地出错——请核实你所用驱动的具体参数风格,而不要想当然。

常见误解

"我用了 ORM,所以自动就安全了"

对 ORM 的常规查询 API 来说是对的——但一旦你使用 raw()text() 并自己拼字符串,这个说法就不成立了。

"这个 ID 一定是数字,所以直接拼进去没问题"

风险并不在于运行时值的类型——而在于查询本身就是通过字符串拼接构造的。一旦这个假设在代码生命周期中的任何一个环节被打破,漏洞早已在那里等着了。

"我自己对引号做了转义,就不需要参数化查询了"

手动转义是驱动相关的,也很容易在不易察觉的地方出错。参数化查询并不是"更严格的转义",它是从根本上避开了这个问题,因为值从未成为查询文本的一部分。

如何检查自己是否受影响

grep -rn "execute(f\"" --include="*.py" . grep -rn "execute(.*%\s*(" --include="*.py" . grep -rn "\.raw(\|text(" --include="*.py" .
比单纯 grep 更好的做法:在 CI 中运行 Bandit(规则 B608,hardcoded_sql_expressions)——它能自动识别这种模式,并在出现新问题时让构建失败。

防范清单

常见问题

使用 ORM 能防止 SQL 注入吗?

对其常规查询方法来说可以。但原生查询的"逃生通道"不行——它们的安全性和手写 SQL 完全一样,并不会更高。

这种风险只存在于搜索框之类的地方吗?

不是。任何实际上由外部方控制的内容都算数——HTTP 请求头、上传文件名,甚至是应用所信任的第三方 API 返回的值。

我能不能自己对引号做转义就行?

可以,但这很脆弱,而且依赖具体驱动。参数化查询才是真正的修复方式,而不是更严格版本的转义。

参考资料

查看语言: Python JavaScriptGoJava PHPC#Ruby C/C++RustKotlin Swift Solidity(不适用)
另请参阅: 命令注入路径遍历 XSS不安全的反序列化