to_sql 不支持参数化插入,仅接受预定义表名;sql 占位符(如%s、?)会报错或静默失败,防注入须用 read_sql + params 或白名单校验表名/列名。

to_sql 默认不执行参数化插入,所谓“参数化选项”根本不存在——它只接受已构造好的 DataFrame,不处理 SQL 语句层面的占位符。 你不能像 read_sql 那样传 params 给 to_sql,强行拼接表名或字段名进 SQL 字符串就是高危操作。
to_sql 的底层机制决定了它不走 SQL 参数化路径
to_sql 的作用是把 DataFrame 转成批量 INSERT 语句(或调用数据库原生 load 接口),全程由 SQLAlchemy 的 Dialect 和底层驱动控制。它不解析、不重写、不插值 SQL 字符串——所以你给它传一个带 ? 或 %s 的字符串,它会直接报错或静默失败。
- 错误示例:
df.to_sql("users WHERE id = %s", con=engine, ...)→ 表名非法,抛DatabaseError - 真实行为:SQLAlchemy 把
table_name当作标识符(identifier)处理,不是参数;字段名、WHERE 条件、ORDER BY 等都不可动态注入 - 唯一可控的“参数化”入口是
if_exists和index这类布尔/枚举型开关,和 SQL 注入防护无关
真正能防注入的只有 read_sql + params
如果你需要根据用户输入动态过滤数据(比如前端传来的日期范围、状态码),必须用 read_sql 配合 params,而不是试图改造 to_sql:
sql = "SELECT * FROM trades WHERE date >= %s AND status IN %s"
df = pd.read_sql(sql, con=engine, params=('2026-01-01', ('filled', 'pending')))
-
params值必须与 SQL 中占位符顺序、类型严格匹配;MySQL 用%s,PostgreSQL 用%s或$1,SQLite 用? - 不要用 f-string 或
.format()拼接sql字符串——哪怕只是拼表名,也等于主动绕过参数化 - 如果表名/字段名真需动态,必须白名单校验:
if table_name not in ['trades', 'positions']: raise ValueError("Invalid table")
to_sql 场景下防注入的实操底线
当你要把 DataFrame 写入数据库时,注入风险其实来自两个隐性环节:构造表名/字段名、预处理 DataFrame 本身是否含恶意 payload。安全动作只在外部:
- 禁止从用户输入直接取
tablename:df.to_sql(request.args.get('table'), ...)是典型雷区 - 对 DataFrame 的列名做清洗:
df.columns = [re.sub(r'[^a-zA-Z0-9_]', '_', c) for c in df.columns],防止列名含反引号或注释符 - 避免使用
method='multi'+ 旧版 PyMySQL:该组合会退化为字符串拼接 INSERT,已知触发`name`; DROP TABLE --类攻击(见 2026 年 3 月 pandas 安全通告) - 强制用 SQLAlchemy
Engine,不用裸 DBAPI2 连接对象——否则to_sql无法启用连接池和统一转义逻辑
最常被忽略的一点:to_sql 不处理 SQL 注入,但 pandas.read_csv(..., engine='python') 配合恶意分隔符可导致任意代码执行——这种“非 SQL 场景下的注入”更容易被当成数据问题放过。










