用%s仍被注入的主因是误将f-string与参数化混用,导致%s未进入驱动参数通道而降级为字符串拼接;另常见错误包括传参非tuple/list、表名列名等语法结构误用%s参数化。

为什么用 %s 却还是被注入了
常见错误是把 %s 当成 Python 的字符串格式化符号——比如写 cursor.execute(f"SELECT * FROM user WHERE id = %s", (user_id,)),其中 f 字符串提前拼接了 SQL 结构,%s 根本没进数据库驱动的参数通道。此时 pymysql 会降级为纯字符串替换,和 "'"+user_id+"'" 没本质区别。
另一个高频坑是传参类型不对:cursor.execute("UPDATE t SET x = %s", "hello") 少了括号,Python 把字符串当可迭代对象拆开传了(相当于传 ('h', 'e', 'l', 'l', 'o')),轻则报错,重则执行意外语句。
- 参数必须是
tuple或list,单值也得写成(value,)(注意末尾逗号) - 绝不能混用 f-string、
.format()、%拼接 SQL 字符串本身 -
%s是 pymysql 唯一支持的占位符;用:name或?会直接报错或触发回退逻辑
cursor.execute() 的参数必须严格匹配 %s 出现顺序
UPDATE 和 INSERT 场景最容易出错。比如 "INSERT INTO log (msg, level, ts) VALUES (%s, %s, %s)",对应参数元组必须是 (msg_str, level_int, datetime_obj),顺序错一位,数据就全插歪了;类型错(比如把字符串传给 INT 字段),MySQL 可能静默转成 0 或截断,查起来像“数据丢了”,实则是参数错位。
- 字段名、表名、排序关键词(
ORDER BY)、条件关键词(IN列表)都不能参数化——这些属于 SQL 语法结构,不是数据,%s不起作用 - 如果真要动态表名,只能白名单校验后字符串拼接,例如
if table_name in ["user", "order", "product"]: sql = f"SELECT * FROM {table_name} WHERE id = %s" -
IN子句需手动展开占位符:传入ids = [1, 5, 9]时,应构造"WHERE id IN (" + ",".join(["%s"] * len(ids)) + ")",再传ids元组
连接配置里 autocommit=False 和事务控制是安全前提
参数化只管“数据不被当代码执行”,但不保证业务逻辑正确。比如转账操作中,扣款和加款两条 UPDATE 必须在同一个事务里,否则中间出错会导致资金不一致。若 autocommit=True,每条语句自动提交,一旦第二条失败,第一条已不可逆。
- 显式开启事务:
conn.begin()或确保autocommit=False后调conn.commit()/conn.rollback() - 使用上下文管理器可减少漏关连接、漏提交的风险:
with conn.cursor() as cursor:自动 close,但commit仍需手动 - 连接池(如 DBUtils.PooledDB)场景下,
conn.close()实际是归还连接,不是销毁,所以事务状态不会跨请求残留——这点常被忽略
ORM 能绕过手写 SQL 的所有参数化陷阱,但别滥用 text()
SQLAlchemy 等 ORM 默认所有查询都是参数化的,filter(User.name == name) 底层生成的 SQL 天然带 %s 占位符。但如果你为了“灵活”硬写原生 SQL:session.execute(text("SELECT * FROM user WHERE name = :name"), {"name": user_input}),虽然用了命名参数,但 text() 绕过了 ORM 的安全层,一旦拼接字符串进去(比如 text(f"SELECT * FROM {table} ...")),防线立刻崩溃。
真正省心的做法是:CRUD 全走 ORM 方法;只有极少数复杂分析查询才用 text(),且必须配合参数绑定、表名列名白名单校验、最小权限数据库账号。


















