flawopen.com/SQL 注入/Python
想象一个表单,本来只期望你填入一个像 482 这样的工单编号。SQL 注入就是当有人没有填入数字,而是输入了一段"伎俩文字",导致系统回应"把所有工单都给我看",而不是只显示 482 号工单——因为系统从未真正检查过收到的内容是不是纯粹的数字。
SQL 注入发生在用户可控输入被直接拼接进数据库查询的文本中,而不是作为独立的值传递时。如果代码通过拼接字符串来构造查询,攻击者就可以提供能改变查询实际结构的输入——把一条"查一行"的查询变成返回所有行,甚至删除整张表的查询。
在 Python 中,这几乎总是以同一种方式出现:用 f-string、% 格式化,或者简单的 + 拼接来构造数据库调用,而不是使用数据库驱动内置的参数占位符。
2015 年,英国电信运营商 TalkTalk 遭遇数据泄露,超过 15 万名客户受到影响,起因是攻击者利用了一个通过收购继承而来的老旧网页中的 SQL 注入漏洞。英国数据保护监管机构对 TalkTalk 处以 40 万英镑罚款,认定这一失误"本可避免且属于基础性错误"。
来源:英国信息专员办公室(ICO)处罚通知,2016 年——详见下方"参考资料"。# 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() 拿到查询时,值早已被烘焙进文本里,就好像它一直就是命令的一部分。
cursor.execute("... WHERE id = %s" % user_id) 和 f-string 一样危险。只有当值作为 execute() 自身的第二个参数传入时,占位符才会真正安全——cursor.execute("...WHERE id = %s", (user_id,))——这样做替换的是驱动本身,而不是 Python 的字符串格式化。
Django 的 ORM 和 SQLAlchemy 的查询构造器在正常查询中都会自动参数化。一旦你使用 Model.objects.raw() 或 SQLAlchemy 的 text(),并用 f-string 拼出这段原生 SQL,风险就又回来了。
psycopg2(PostgreSQL)不论列类型一律使用 %s;sqlite3 使用 ?。把某个驱动文档里的占位符风格照搬到另一个驱动上,会悄无声息地出错——请核实你所用驱动的具体参数风格,而不要想当然。
对 ORM 的常规查询 API 来说是对的——但一旦你使用 raw() 或 text() 并自己拼字符串,这个说法就不成立了。
风险并不在于运行时值的类型——而在于查询本身就是通过字符串拼接构造的。一旦这个假设在代码生命周期中的任何一个环节被打破,漏洞早已在那里等着了。
手动转义是驱动相关的,也很容易在不易察觉的地方出错。参数化查询并不是"更严格的转义",它是从根本上避开了这个问题,因为值从未成为查询文本的一部分。
grep -rn "execute(f\"" --include="*.py" .
grep -rn "execute(.*%\s*(" --include="*.py" .
grep -rn "\.raw(\|text(" --include="*.py" .
对其常规查询方法来说可以。但原生查询的"逃生通道"不行——它们的安全性和手写 SQL 完全一样,并不会更高。
不是。任何实际上由外部方控制的内容都算数——HTTP 请求头、上传文件名,甚至是应用所信任的第三方 API 返回的值。
可以,但这很脆弱,而且依赖具体驱动。参数化查询才是真正的修复方式,而不是更严格版本的转义。