Peewee执行原生SQL必须参数化:execute_sql()需传参而非字符串拼接;raw_query()不支持动态表名列名,须白名单校验;fn/SQL类混入用户输入即危险;事务中执行需注意版本兼容性及ORM状态同步。

用 execute_sql() 执行原生 SQL 时必须参数化
Peewee 的 execute_sql() 不自动做参数绑定,直接拼接字符串等于敞开 SQL 注入大门。比如写 db.execute_sql(f"SELECT * FROM user WHERE name = '{name}'"),只要 name 来自用户输入(表单、URL 参数、JSON body),就极大概率被利用。
正确做法是把值作为第二个参数传入,由 Peewee 底层用数据库驱动的参数化机制处理:
db.execute_sql("SELECT * FROM user WHERE name = ? AND status = ?", name, status)注意:不同数据库占位符不同 —— SQLite/MySQL 用 ?,PostgreSQL 用 %s,但 Peewee 会根据 Database 实例类型自动适配,你只需统一写 ? 即可(除非显式用了 PostgresqlDatabase 且手动调用底层游标)。
避免在 raw_query() 中拼接字段名或表名
raw_query() 返回的是 Query 对象,常用于复杂查询,但它只对 WHERE / VALUES 部分支持参数化,不接受表名、列名、ORDER BY 字段等动态标识符。
如果必须动态指定表名(比如按租户分表),不能这样写:
raw_query(f"SELECT * FROM {table_name} WHERE id = ?", user_id) // ❌ 危险而应提前白名单校验:
- 维护一个允许的表名集合:
ALLOWED_TABLES = {"user_v1", "user_v2", "order_2026"} - 校验:
if table_name not in ALLOWED_TABLES: raise ValueError("Invalid table") - 再拼接:
raw_query(f"SELECT * FROM {table_name} WHERE id = ?", user_id)
字段名同理,别信“用户只输字母数字”这种假设 —— Unicode 控制字符、反引号、括号都可能绕过简单正则。
慎用 fn 和 SQL 类构造动态表达式
fn.COUNT() 或 SQL("NOW()") 看似安全,但一旦混入用户输入就失效。例如:
fn.CONCAT("Hello ", user_input) // ❌ user_input 若为 "World' -- ",可能破坏语义真正安全的组合是:
- 固定函数模板 + 参数化值,如
fn.DATE_ADD(fn.CURDATE(), fn.INTERVAL(days, 'DAY'))中days是整数变量,会被当作参数传入 - 绝对不用
SQL(f"ORDER BY {sort_field}"),改用白名单映射:sort_map = {"name": "name ASC", "age": "age DESC"},再SQL(sort_map.get(sort_by, "id"))
Peewee 的 SQL 类本身不解析内容,它只是原样透传,等价于手写字符串 —— 安全性完全取决于你怎么喂它。
连接池与事务中执行原生 SQL 的额外注意点
在 db.atomic() 或 db.manual_commit() 块里调用 execute_sql(),不会自动参与事务管理,除非你用的是 Peewee 3.14+ 且数据库驱动支持(如 psycopg2)。低版本或 SQLite 可能静默提交。
更隐蔽的风险是:混合 ORM 操作和原生 SQL 时,ORM 的缓存、实例状态不会感知原生修改。例如:
user = User.get(User.id == 1)
db.execute_sql("UPDATE user SET name = ? WHERE id = ?", "NewName", 1)
print(user.name) // 仍是旧值,ORM 实例未刷新所以,涉及关键状态变更时,要么全用 ORM,要么执行完原生 SQL 后主动 User.get() 刷新,或用 db.execute_sql() 配合 fetchone() 拿结果,别依赖 ORM 实例的陈旧数据。

















