bigywater.tech
Flask · 124

SQL Injection 与参数化查询

为什么要用参数化查询?ORM 是不是自动安全?哪些位置参数化帮不上忙?这篇不演示攻击,只用一个名字里带单引号的正常用户,看拼接与绑定在真实的 PostgreSQL 里各自发生了什么。

$1
PostgreSQL 日志里 text() 绑定参数的执行形式(服务端绑定)
白名单
表名、列名、排序方向这些不能参数化的位置的处理方式
autoescape
LIKE 里转义 % 与 _ 的选项

拼接与绑定

实测 · 本机执行输出
  f-string 拼接 → ProgrammingError: syntax error at or near "Brien"
  绑定参数 :n → [(1,)]
  发给数据库的语句与参数: ('select id from "user" where name = %(n)s', {'n': "O'Brien"})
  ORM 表达式 → [(1,)]
  发给数据库的语句与参数: ('SELECT "user".id FROM "user" WHERE "user".name = %(name_1)s::VARCHAR', {'name_1': "O'Brien"})
  结论:引号被当作数据,不需要自己转义;拼接则把数据当成了 SQL 的一部分,正常输入也会出错

上一篇:XSS 防护;下一篇:路径穿越与文件下载。

不能参数化的位置

实测 · 本机执行输出
  排序列 'name' asc → ['50%_off', 'Ann', 'Bob', "O'Brien"]
  排序列 'id' desc → ['Bob', '50%_off', 'Ann', "O'Brien"]
  排序列 'password' asc → 拒绝:不允许按 'password' 排序
  把列名当绑定参数 order by :c → ["O'Brien", 'Ann', '50%_off', 'Bob'] (只是按常量字符串排序,不会按列排序)

LIKE 与 IN

实测 · 本机执行输出
  like('%50%_off%')(不转义,% 与 _ 当通配符)→ ['50%_off']
  contains(term, autoescape=True) → ['50%_off']
  发出的语句: HERE ("user".name LIKE '%%' || %(name_1)s::VARCHAR || '%%' ESCAPE '/') {'name_1': '50/%/_off'}
  in_([...]) → ["O'Brien", 'Ann', 'Bob'] | 语句: _1)s::VARCHAR, %(name_1_2)s::VARCHAR, %(name_1_3)s::VARCHAR)

服务端日志

实测 · 本机执行输出
   PG 日志: 2026-10-02 14:00:15.243 CST [23880] STATEMENT:  select id from "user" where name = 'O'Brien'
   PG 日志: execute <unnamed>: select id from "user" where name = $1

本篇只做防御向的验证:没有构造任何利用语句。

分步演示

SQL 参数化查询
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SQLAlchemy 3.1.1、psycopg 3.3.6、自己起的 PostgreSQL(非默认端口,临时打开了 log_statement)
    • 没有验证:存储过程与动态 SQL(EXECUTE format)里的拼接、ORM 的 text() 片段里混入变量、JSON 路径与 order_by(text(...)) 这类容易被拼接的 API、数据库账号最小权限的缓解作用、WAF
    京ICP备2026067502号-1京公网安备11010502063487号