动态表名、字段名及order by字段必须白名单校验,因数据库驱动不支持参数化;应使用预定义集合校验,如if table_name not in ["users", "orders", "logs"],并用映射表控制别名与字段集限制。

动态表名和字段名必须走白名单校验
表名、字段名、ORDER BY 字段这些属于 SQL 结构部分,所有数据库驱动(pymysql、psycopg2、sqlite3)都不支持参数化。直接拼接 f"SELECT * FROM {table_name}" 就等于开门揖盗。
正确做法是用硬编码字典或预定义集合做校验:
- if table_name not in ["users", "orders", "logs"]: raise ValueError("非法表名")
- 用映射表控制别名:
table_map = {"user_v1": "users_2024", "order_main": "orders_prod"},只允许从 key 中取值 - 字段名同理,
allowed_fields = {"id", "name", "status", "created_at"},再用set(columns) 判断
数据值一律用 %s 占位符 + 元组传参(pymysql / mysql-connector)
pymysql 只认 %s,不接受 ? 或 %(name)s(后者是 psycopg2 的风格)。写错占位符会导致降级为字符串拼接,形同虚设。
常见错误包括:
- 混用 f-string 和 %s:
f"WHERE id = %s AND name = '{name}'"—— 后半段已失控 - 对整数字段误用
%d:"WHERE age = %d"会报错,且破坏参数绑定机制 - 传参类型错误:传入列表而非元组,如
cursor.execute(sql, [1, 2]),部分版本会静默失败
安全写法示例:cursor.execute("UPDATE %s SET status = %s WHERE id IN %s", (table_name_safe, "done", tuple(id_list))) —— 注意:IN 后不能直接传元组,需动态生成对应数量的 %s 占位符
IN 子句要按实际长度构造占位符
WHERE id IN (%s, %s, %s) 这种结构无法靠单个 %s 替代,数据库不支持“可变长参数占位符”。传入 (1, 2, 3) 不会自动展开成三个值。
必须手动构造占位符串:
placeholders = ",".join(["%s"] * len(ids))sql = f"SELECT * FROM users WHERE id IN ({placeholders})"cursor.execute(sql, tuple(ids))
注意:ids 必须是可信来源(如迁移配置里写死的 ID 范围),否则仍需先校验每个元素是否为正整数
绝对禁用 addslashes() 和 mysql_real_escape_string()
这些函数在数据迁移场景中尤其危险——它们只处理单引号和反斜杠,对 --、#、/* */ 注释符或堆叠查询(;)完全无效。更糟的是,多字节编码下还存在绕过漏洞。
典型误用:sql = "INSERT INTO logs VALUES ('" + addslashes(user_ip) + "'),当 user_ip = "127.0.0.1' --" 时,注释掉后续校验逻辑。
正确做法:
- 全量删除项目中所有
addslashes()、mysql_real_escape_string()调用 - 改用参数化;若必须拼 DDL(如
CREATE TABLE),则用模板 + 白名单键名,例如:template_map = {"log_daily": "CREATE TABLE logs_202409 (id BIGINT)"} -
psycopg2用户可启用sql.SQL()+sql.Identifier()安全拼接标识符,但需显式导入from psycopg2 import sql,不是默认可用
动态 SQL 最容易被忽略的点,是把“能跑通”当成“安全”——哪怕测试数据全是字母数字,只要脚本接收外部输入(命令行参数、CSV 头、JSON 配置),就必须按白名单+参数化双重防线来约束。没有例外。










