动态表名不能用 ? 占位符,因JDBC驱动仅支持对值参数化,不支持对表名等SQL结构参数化;硬写“SELECT * FROM ?”会直接报语法错误;必须通过严格白名单校验(如Set.of("users","orders"))并在SQL构建入口强制拦截。

动态表名为什么不能用 ? 占位符
JdbcTemplate 和 MyBatis 的 ? 或 #{} 只绑定「值」,不参与 SQL 语法解析。表名、列名、ORDER BY 子句属于 SQL 结构,JDBC 驱动根本不允许参数化这些位置。硬写 "SELECT * FROM ? WHERE id = ?" 会直接抛 SQLException:「Parameter index out of range」或「You have an error in your SQL syntax」。
白名单校验必须严格到字符级
别用模糊正则(如 sortField.matches("[a-zA-Z_]+")),攻击者填 id ASC, (SELECT password FROM users) 就能绕过。正确做法是预定义静态集合,并做完全匹配:
private static final Set<string> ALLOWED_TABLES = Set.of("users", "products", "orders");</string>- 校验逻辑必须写成:
if (!ALLOWED_TABLES.contains(tableName)) { throw new IllegalArgumentException("Invalid table name"); } - 排序字段同理:只允许
"id"、"created_at"、"status";sortOrder只允许"ASC"或"DESC",且需单独校验,不能拼接后一起判断
MyBatis 中 ${} 不是“可用但要小心”,而是“只准白名单后才许用”
${} 是运行时字符串替换,没有预编译,数据库收到的就是完整语句。哪怕只拼表名,只要没白名单兜底,就等于裸奔。错误示例:<select>SELECT * FROM ${tableName} WHERE id = #{id}</select>——#{id} 再安全也救不了 ${tableName}。
正确姿势:
- 建映射表:
Map.of("user", "t_user", "order", "t_order") - 在 Java 层查表名:
String realTable = TABLE_ALIAS_MAP.getOrDefault(inputTable, null); if (realTable == null) throw ...; - XML 中只写固定值:
SELECT * FROM ${realTable} WHERE id = #{id},且确保realTable来自白名单映射,而非原始输入
JdbcTemplate 动态表名拼接前必须拦截原始请求
很多人以为「我在 service 层校验了表名,就安全了」,但忘了攻击可能来自 JSON Body、Header、Path Variable 或文件上传字段。HandlerInterceptor 拿不到原始 body,必须用 @WebFilter 在 Servlet 容器最前端介入:
- 注册过滤器:
@WebFilter(urlPatterns = "/*"),覆盖全部入口(含 API 路径) - 同时处理:
getInputStream()(JSON)、getReader()(XML)、getPart()(multipart 表单字段) - 对所有提取出的
tableName字段,立即走白名单校验,失败则response.sendError(400)
白名单不是配置项开关,是代码里写死的不可变集合;任何试图从数据库、配置中心、Redis 动态加载白名单的行为,都会引入新的注入面。


















