UNION注入需匹配字段数量和类型,因数据库执行时会隐式列对齐,字段数不等报错,类型不兼容导致转换失败;攻击者通过ORDER BY和UNION SELECT试错探测结构;防御核心是参数化查询与白名单校验。

为什么UNION注入必须匹配字段数量和类型
因为数据库执行 UNION 时会做隐式列对齐:左右两个查询的字段数必须一致,否则报错如 SQLSTATE[HY000]: General error: 1222 The used SELECT statements have a different number of columns;同时,对应位置的字段类型需兼容(比如不能把字符串和日期直接并列),否则可能触发类型转换失败或空结果。
攻击者正是靠反复试错 ORDER BY n 和 UNION SELECT 1,2,3... 来探出真实查询的字段数和大致类型。防御的第一道防线,就是不让这个探针生效。
用预编译参数化查询彻底隔离用户输入
绝大多数 UNION 注入发生在拼接 SQL 字符串的场景,比如 PHP 中的 $sql = "SELECT name FROM users WHERE id = " . $_GET['id']。只要改用参数化查询,用户输入就永远进不了 SQL 结构层,UNION 根本没机会被解析为关键字。
- MySQLi:用
prepare()+bind_param(),参数只作为值,不参与语法构建 - PDO:用
prepare()+execute(),占位符?或:name不会被解释为 SQL 片段 - 切忌在
WHERE、ORDER BY、GROUP BY等子句中参数化——这些位置不支持参数绑定,必须白名单校验或重写逻辑
对动态列名/表名等无法参数化的部分做严格白名单控制
当业务真需要动态指定排序字段(如 ORDER BY ? 不合法),就必须放弃自由输入,只允许从预设集合中选:['id', 'name', 'created_at']。任何不在列表中的值直接拒绝,不尝试“智能过滤”或正则替换。
常见错误是用 preg_replace('/[^a-zA-Z0-9_]/', '', $input) 清洗,这仍可能留下 user_id 这类合法字段名,但若后端逻辑又拼接了额外条件(如 "AND status = 1"),攻击者可构造 user_id UNION SELECT ... 绕过。
- 白名单必须基于语义,而非字符集:比如排序字段只接受已知索引列,且不包含计算字段或别名
- 若需支持多语言字段别名,应在应用层映射,而非将别名透传到 SQL
- 绝不信任客户端传来的任何结构相关字段,包括分页参数中的
limit、offset,它们虽不触碰字段数,但配合UNION可用于盲注数据提取
应用层验证查询结果集结构(仅限ORM或封装层)
这不是通用防御手段,仅适用于你完全控制查询执行路径的场景,比如自研 DAO 层或重度封装的 ORM。可以在执行前检查 SQL AST 或执行后校验元数据:
- 执行
SELECT * FROM ...后调用mysqli_fetch_fields()或PDOStatement::getColumnMeta(),确认返回字段数与预期一致(如业务明确只需 3 列,结果却有 5 列就告警) - 对字段类型做粗略检查:比如预期全是
STRING和INT,却出现BLOB或NULL占位,可能是UNION SELECT NULL,NULL,...的痕迹 - 注意:这种校验无法防止注入本身,只能辅助发现异常行为;且
getColumnMeta()在 MySQL 中部分版本不返回准确类型,PHP 8.1+ 才较可靠
真正有效的防御永远落在输入处理层——字段数和类型匹配只是攻击链上的一环,盯住源头比补救末端更省力也更可靠。别让验证逻辑变成新攻击面。

















