安全路径只有一条:白名单校验必须前置、严格、不可绕过。表名列名无法参数化,因属sql语法结构;${}拼接须经硬编码白名单映射,分表租户场景须服务端生成+认证上下文提取,白名单严禁动态加载且需人工强审。

动态表名和列名拼接是SQL注入高危区,因为 PreparedStatement 和 #{} 完全无法覆盖——数据库引擎根本不允许把 @table_name 当参数传,硬写就会报错,比如 Incorrect syntax near '@' 或 You have an error in your SQL syntax。安全路径只有一条:白名单校验必须前置、严格、不可绕过。
为什么不能对表名/列名做参数化
参数化查询(?、@param、#{} )只作用于“值”位置,而表名、列名、ORDER BY 字段属于 SQL 语法结构。数据库在解析阶段就需要确定这些标识符,不可能等到执行时再代入。所以:
-
SELECT * FROM ? WHERE id = ?—— 第一个?在所有主流驱动中都非法 -
MyBatis中${tableName}是字符串替换,不是占位,拼之前没校验就等于裸奔 -
QUOTENAME()或backtick转义只防语法错误,不防逻辑劫持(如users; DROP TABLE logs--)
白名单必须硬编码且全覆盖
白名单不是“过滤非法字符”,而是明确声明哪些输入可被接受。正则、黑名单、REPLACE 都不可靠,攻击者能用大小写、双下划线、Unicode 变体绕过。正确做法是:
- 用常量映射表,例如
Map.of("order", "orders_2024", "user", "users"),键来自前端路径或字段,值才是真实表名 - 列名白名单要包含所有业务允许的排序/分组字段,比如
Set.of("id", "created_at", "status"),连user_id这种常见但未声明的也得拒掉 - 映射逻辑必须在 DAO 层最外侧完成,不能到 MyBatis XML 或存储过程中才校验
分表/租户场景下的表名生成规则
真有按时间或租户分表需求(如 logs_202409、tenant_123_events),表名不能由用户输入直接参与拼接:
- 年月部分必须由服务端计算得出,例如
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM")) - 租户 ID 必须从认证上下文(如 JWT payload)提取,并在校验环节比对预注册租户列表,而不是拿原始参数去拼
- 禁止任何形式的“动态加载白名单”——配置不能来自数据库、Redis 或外部 API,否则等于把后门钥匙交出去
最容易被忽略的一点:白名单漏加一个字段或拼错一个表名,线上就直接报错;加多一个没审核过的别名,就等于开了个注入口。这事没法靠工具兜底,只能人盯人、Code Review 强制卡点、上线前走白名单变更清单。











