因为?和#{}仅支持参数化“值”,而schema、table名属于sql标识符,需在语法分析阶段确定,故preparedstatement无法处理;必须通过白名单校验加方言特定转义(如mysql反引号)来保障安全。

不能用 ? 或 #{} 绑定 schema、table 名——这是 SQL 解析器的硬限制,不是框架或驱动的缺陷。
为什么 PreparedStatement 对表名无效
SQL 预编译只对「值」做参数化,WHERE name = ? 中的 ? 是占位数据;而 FROM ? 会直接报错,因为解析器在语法分析阶段就需要知道对象结构(schema/table 是标识符,不是值)。MyBatis 的 #{} 底层仍是 JDBC PreparedStatement,所以同样不支持。
- MySQL 报错:
ERROR 1064 (42000): You have an error in your SQL syntax - Oracle 报错:
ORA-00903: invalid table name - DB2 报错:
SQL0104N An unexpected token "..." was found
白名单校验必须配合标识符转义
仅用正则或字符串匹配(如 tableName.matches("[a-zA-Z_][a-zA-Z0-9_]*"))远远不够。攻击者可构造合法但危险的名称(如 user; DROP TABLE accounts --),正则可能放过;更隐蔽的是第二阶注入:校验通过后拼进 SQL,再被后续逻辑二次解释。
- DB2 必须用双引号包裹:
"PROD@ABC"."EMPLOYEE",否则含@、-的 schema 名会解析失败 - Oracle 要求
DBMS_ASSERT.QUALIFIED_SQL_NAME('hr.employees'),但该函数不检查对象是否存在,仅校验语法 - MySQL 可用反引号:
`my-table`,但需确保输入已通过白名单(如Set.of("orders_v1", "orders_v2"))
MyBatis 动态表名的两种安全写法
用 ${} 不等于放任不管,关键在于控制替换源的可信边界。
- 硬编码分支:
<choose><when test="tableName == 'user_log'>FROM user_log</when><when test=" tablename="=">FROM user_audit</when></choose>—— 完全规避外部输入参与拼接 - 白名单 + 转义组合:
SELECT * FROM ${@com.example.util.SqlIdentifierSanitizer@quoteSchemaTable(schemaName, tableName)},其中quoteSchemaTable先查VALID_TABLES.contains(tableName),再按方言加引号
最易被忽略的点:白名单必须是 private static final Set 或配置中心下发的不可变集合,不能是运行时从数据库查出的动态列表——后者一旦被污染,整个防线就垮了。











