django orm 安全前提是使用 filter()/get() 等方法,避免 extra() 字符串拼接和 raw() 未参数化调用;sqlalchemy 需用 text() 配命名占位符与字典传参;原生驱动必须通过参数化接口传值,动态结构部分(表名、排序字段等)须白名单校验。

用 Django ORM 就别手拼 SQL 字符串
只要调用 filter()、get()、exclude() 这类方法,Django 会自动参数化所有查询值,底层走的是预编译语句。哪怕用户输入 "'; DROP TABLE users; --",也会被当作文本字面量处理,不会执行。
常见错误现象:有人在 extra() 里写 where=["name = '%s'" % user_input],或用 raw() 拼接字符串——这等于主动绕过防护。
- 绝对避免
raw()+ 字符串插值;必须用时,只接受params参数传值,如MyModel.objects.raw("SELECT * FROM my_table WHERE id = %s", [user_id]) -
extra()已弃用,改用annotate()或Q()组合条件 - 自定义 SQL 场景(比如复杂窗口函数)优先走数据库视图 + 模型映射,而非运行时拼接
SQLAlchemy 的 text() 不等于安全,得配 bindparam
text() 本身只是语法糖,不带自动转义。直接 text("SELECT * FROM users WHERE name = '" + name + "'") 和裸写 Python 字符串一样危险。
正确做法是显式声明参数占位符,并通过执行时传参触发绑定:
from sqlalchemy import text
stmt = text("SELECT * FROM users WHERE status = :status AND age > :min_age")
result = conn.execute(stmt, {"status": "active", "min_age": 18})
关键点在于::status 这种命名占位符 + 字典传参,才能触发 SQLAlchemy 底层的参数化处理;用 ? 位置占位符也行,但可读性差,容易错位。
- 别用
f"..."或%拼进text()字符串里 - 动态表名/字段名不能参数化(SQL 标准限制),需白名单校验,例如
if table_name not in ["users", "orders"]:抛异常 - 批量插入用
executemany()而非循环调用execute(),性能高且仍受参数化保护
原生 MySQL/PostgreSQL 驱动的 execute() 必须用参数占位符
Python 的 mysql-connector-python 和 psycopg2 都支持两种占位符:%s(MySQL)和 %s 或 $1(PostgreSQL),但都要求值通过第二个参数传入,而非字符串格式化。
错误示范:cursor.execute(f"SELECT * FROM logs WHERE ip = '{user_ip}'") —— 单引号一加,注入就进来了。
- MySQL:只认
%s,用列表或元组传参,如cursor.execute("SELECT * FROM t WHERE id = %s", [123]) - PostgreSQL:推荐用
%s(兼容 psycopg2 和 asyncpg),避免混用$1导致迁移困难 - 多值 IN 查询不能直接展开,得用
tuple(values)+ 动态生成占位符串,例如"WHERE id IN ({})".format(",".join(["%s"] * len(ids)))
ORM 并不解决所有注入问题,动态查询构建仍是高危区
ORM 安全的前提是“查询结构固定”。一旦开始拼接表名、字段名、ORDER BY 字段、LIMIT 偏移量,就脱离了参数化范围——这些属于 SQL 结构部分,数据库不允许运行时参数化。
典型场景:后台搜索页支持按任意字段排序,用户传 sort=price; DROP TABLE products;--,后端若直接拼进 ORDER BY {sort},就中招了。
- 排序字段必须白名单校验:
if sort_field not in ["created_at", "price", "name"]:拒绝请求 - LIMIT/OFFSET 用
int()强转并设上限,防止超大数字拖垮数据库 - JSON 字段查询(如 PostgreSQL 的
->>操作符)里的键名也要校验,不能由用户直接输入
最麻烦的其实是嵌套 JSON 查询和动态字段投影,这里没标准解法,靠设计阶段就约束接口粒度,而不是到实现时硬扛。










