彻底消除动态SQL拼接是防御SQL注入的底线,因预编译仅参数化值而非SQL结构;拼接表名、列名、ORDER BY等位置会使参数化完全失效,必须用白名单校验替代。

彻底消除动态SQL拼接不是“理想状态”,而是防御SQL注入的底线——只要拼接存在,参数化就形同虚设。
动态拼接表名或列名时,参数化完全失效
预编译语句(PreparedStatement)只对**值**做参数化,不处理 SQL 结构本身。一旦你把用户输入塞进 FROM、ORDER BY、GROUP BY 或 SELECT 列列表里,数据库引擎在解析阶段就会把那段字符串当作语法的一部分执行。
- 错误示例:
"SELECT * FROM " + userInputTable + " WHERE id = ?"——userInputTable是攻击者可控的,?对它毫无约束力 - 后果:传入
users; DROP TABLE logs--可能直接触发多语句执行(尤其在 MySQL 的allowMultiQueries=true场景下) - 替代方案:用白名单校验 + 映射,比如
Map.of("orders", "public.orders", "users", "public.users"),不在列表中则拒绝
字符串拼接 + 参数化 = 自欺欺人
常见误区是“我用了 ?,所以安全”。但若拼接发生在 ? 之外的位置,整个防护就崩塌了。
- 危险写法:
"SELECT * FROM users WHERE " + conditionField + " = ?",其中conditionField来自请求参数 - 攻击者传
conditionField=admin' OR '1'='1,最终变成:WHERE admin' OR '1'='1 = ?—— 虽然语法报错,但已暴露结构,且可能绕过后续逻辑 - 更隐蔽风险:拼接用于
LIMIT、OFFSET或UNION SELECT子句,这类位置连类型检查都难覆盖
ORM 和 PgTyped 也救不了拼接逻辑
即使你用的是 PgTyped 或 Hibernate,只要代码里出现 `SELECT * FROM ${tableName}` 这类模板字符串,工具生成的类型定义和参数绑定全部失效。
-
PgTyped的安全前提是所有 SQL 在编译期固定,它无法为运行时拼接的查询生成类型约束 - MyBatis 中误用
${}(非#{})等价于裸拼接,Spring Boot 日志里甚至不会警告 - Node.js 的
pg库中,client.query("SELECT * FROM " + table, params)不会报错,但等于开门揖盗
真正难的不是写对一个查询,而是在整个代码库中杜绝“这里应该没问题”的侥幸——表名、排序字段、条件分组、CTE 别名……任何参与 SQL 语法构建的变量,都必须经过静态校验或明确拒绝外部输入。

















