动态SQL注入漏洞主要源于IN条件拼接和动态表名/字段名替换,必须用参数化查询(如PDO数组绑定、SQLAlchemy in_、MyBatis #{id})和硬编码白名单校验,严禁字符串拼接与不安全转义。

报表系统里动态 SQL 注入漏洞,90% 以上出在 IN 条件拼接和配置驱动的表名/字段名替换上。不改构造逻辑,只加过滤或转义,等于给炸弹贴胶带——表面安全,一碰就炸。
多选下拉框生成的 IN 条件必须用参数化数组
前端传过来的 user_ids=1,2,5 或 ["1","2","5"],后端绝不能走 implode(',', $ids) 拼进 SQL。哪怕每个值都 intval() 过,攻击者仍可提交 1,2,5' OR '1'='1,最终变成 IN (1,2,5' OR '1'='1)。
- PHP PDO 正确做法:确认
$ids是数组 → 生成占位符字符串?, ?, ?→$stmt->execute($ids),让驱动批量绑定 - Python SQLAlchemy:直接传
user_ids列表给User.id.in_(user_ids),框架自动处理 - Java MyBatis:用
<foreach>标签 +#{id}(不是${id}),确保每个值走预编译 - 空数组、
null、超长数组(如 10000 个 ID)必须提前拦截,否则触发语法错误或 DoS
配置表读取的表名/字段名必须硬编码白名单校验
报表常从配置表读 log_table_name 或 tenant_prefix 动态拼表名,比如 SELECT * FROM ${config.tableName}。只要配置被低权限账号篡改,${} 就是执行任意 SQL 的入口。
- 校验必须在 DAO 层之前完成,且白名单必须是代码内硬编码,例如
List.of("user_log_2026", "order_event_v2"),不能从 DB 或 Redis 动态加载 - 正则必须严格:
^[a-z][a-z0-9_]{2,31}$(小写字母开头、3–32 字符、仅字母数字下划线) - 校验失败立即抛
IllegalArgumentException,绝不允许进入 SQL 构造环节 - 通过后才用
String.format("SELECT * FROM %s WHERE id = ?", safeTableName),?只留给数据值
别信“前端限制 = 安全”,所有输入都当恶意处理
下拉框选项前端渲染为 1:张三, 2:李四,不代表后端能信任 1,2 是合法 ID。攻击者可绕过前端直接发请求,塞入 1,2,5' UNION SELECT password FROM users --。
- 所有用户可控输入(含 URL 参数、表单字段、JSON body)都视为不可信源
- 类型校验(如
is_numeric())不能替代参数化,它只防误操作,不防注入 - 允许字母数字组合的业务字段(如部门编码
HR-001)必须单独定义白名单规则,不能一刀切intval() - 日志中记录原始输入值,但绝不回显或用于 SQL 拼接
真正难的不是写对那几行参数化代码,而是把“所有动态结构都不可信”这个意识刻进每一层调用链——从 Controller 接收参数开始,到 DAO 执行前结束,中间任何一次字符串拼接都是雷区。

















