4. SQL 注入防御

SQL 注入防御 学习卡片

ORM 过滤条件与原生 SQL 的边界

ORM 的过滤条件(如 Django 的 filter()、SQLAlchemy 的 where())在正常使用时会把值作为参数绑定,不会把用户输入解释为 SQL 语法。真正的注入风险来自两类写法:一是把用户输入拼进 extra()、raw()、text() 等原生 SQL 片段;二是把用户输入当作字段名、排序方向、表名等标识符传给 ORM,因为参数绑定只能保护值,不能保护标识符。

ORM 查询安全边界:值 vs 标识符 用户输入 值 / 标识符 输入用途判断 值?标识符? 参数绑定(安全) filter(name=user_input) 数据库执行 值作为参数传入 标识符拼接(危险) order_by(user_input) SQL 语法注入 标识符被解释 原生 SQL 片段(raw / extra / text) 字符串拼接 "WHERE id=" + uid 占位符绑定 "WHERE id=%s", [uid] ORM 表达式 Q(id=uid) / where(col==uid) 安全边界规则 值 → 始终用参数绑定 / ORM 过滤条件 标识符 → 白名单校验,禁止直接拼接用户输入 原生 SQL → 必须用占位符,禁止字符串拼接 ORM 表达式 → 优先使用,减少原生 SQL 暴露面

危险写法与安全改写

下面用 Django ORM 和 SQLAlchemy 两个常见框架展示典型风险点。示例中的 request.GET 代表任意用户可控输入。

风险写法 1:把用户输入拼进 extra() 的 where 子句
qs = User.objects.extra(where=[f"name = '{name}'"])
当 name 为 ' OR '1'='1 时,where 子句被改写为永真条件,返回全部用户。
风险写法 2:把用户输入当作排序字段
qs = User.objects.order_by(request.GET['sort'])
当 sort 为 (SELECT ...) 或带子查询的表达式时,可能触发基于时间的盲注或数据泄露。
风险写法 3:SQLAlchemy 中拼接 text()
db.session.execute(text(f"SELECT * FROM users WHERE name = '{name}'"))
与字符串拼接 SQL 等价,参数绑定完全失效。

安全改写方案

值过滤:直接使用 ORM 的过滤条件,框架会自动生成绑定参数。

# Django:安全,name 作为参数绑定
qs = User.objects.filter(name=name)

# SQLAlchemy:安全,name 作为绑定参数
stmt = select(User).where(User.name == name)

原生 SQL 中的值:必须使用占位符,把值放在参数列表中,不要拼进 SQL 字符串。

# Django raw:%s 占位符 + params 列表
qs = User.objects.raw("SELECT * FROM auth_user WHERE name = %s", [name])

# SQLAlchemy text::name 命名占位符 + 参数字典
db.session.execute(
    text("SELECT * FROM users WHERE name = :name"),
    {"name": name}
)

标识符(排序字段、表名、列名):参数绑定无法保护标识符,必须用白名单映射。

ALLOWED_SORT = {
    "name": "name",
    "created": "-date_joined",
    "email": "email",
}

sort_key = request.GET.get("sort", "name")
order_field = ALLOWED_SORT.get(sort_key, "name")  # 非法值回退默认
qs = User.objects.order_by(order_field)
判断口诀:问自己“这个输入会变成 SQL 里的值还是结构?”——值走参数绑定,结构走白名单。两者都不能靠转义或过滤特殊字符来兜底。

验证注入是否失效

改造完成后,用以下 payload 验证:

测试点Payload预期结果
值过滤(name)' OR '1'='1返回 0 条记录,不返回全部用户
排序字段(sort)name; DROP TABLE users--回退默认排序,无异常、无数据破坏
原生 SQL 参数' UNION SELECT password FROM users--作为普通字符串匹配,不执行 UNION
正常输入alice正确返回 alice 的记录

落地检查清单

  • 搜索代码库中所有 extra(、raw(、text(、execute( 调用,确认没有 f-string 或 + 拼接用户输入。
  • 检查所有 order_by、values、annotate 的参数是否来自用户输入,若是则加白名单映射。
  • 确认 ORM 过滤条件中传入的是值而不是格式化后的字符串。
  • 对每个原生 SQL 调用点,确认占位符与参数列表一一对应,且参数来自变量而非拼接结果。
  • 在测试环境用上表 payload 回归,确认注入路径全部失效且正常功能不受影响。