仅做数字类型检查远远不够,因攻击者可用合法数字构造union注入载荷(如“1 union select...”),只要sql拼接存在,类型校验即失效;防御必须依赖参数化查询,覆盖所有用户输入源。

为什么只做数字类型检查远远不够
单纯对用户输入做 is_numeric() 或正则匹配 /^\d+$/,只能拦住明显非数字的字符串(比如 1' OR 1=1--),但完全无法防御 Union 注入中大量合法数字构造的攻击载荷。例如:1 UNION SELECT username,password FROM users-- —— 整个 payload 开头仍是纯数字 1,后续恶意部分被当作“URL参数值”或“GET参数内容”照常拼接进 SQL,检查直接失效。
Union注入真正依赖的两个硬性条件
攻击者能成功执行 Union 注入,必须同时满足:
- 原始查询返回的列数,与
UNION SELECT后面的列数一致(否则数据库直接报错ERROR 1222 (21000): The used SELECT statements have a different number of columns) - 对应列的数据类型兼容(比如原查询第二列是
VARCHAR,你不能在UNION SELECT的第二列填123而不加引号,否则可能报类型转换错误)
这意味着:哪怕你把输入限定为“纯整数”,只要后端用字符串拼接方式构造 SQL(如 "SELECT * FROM news WHERE id = " . $_GET['id']),攻击者仍可输入 1 AND FALSE UNION SELECT 1,2,3,4-- —— 前半部分让原查询无结果,后半部分接管输出,且全部由数字构成。
数字型参数也必须走参数化查询
对 ID、page、limit 这类天然应为数字的字段,开发者常误以为“我已 intval() 过,很安全”。但问题不在输入是不是数字,而在于 SQL 是否被拼接。正确做法只有一条:
- 所有数据库交互,无论输入看起来多“干净”,都必须使用语言原生支持的预编译机制:
PreparedStatement(Java)、pg_query_params()(PHP/PgSQL)、cursor.execute(sql, params)(Python/psycopg2) - 禁用任何
mysql_query("SELECT * FROM t WHERE id = " . $id)类拼接写法,哪怕$id经过(int)$id强转 - ORM 框架如 MyBatis 的
#{}、Hibernate 的:param是安全的;但${}或concat()动态拼表名/字段名仍属高危
参数化不是“锦上添花”,它是唯一能从执行机制上切断输入与 SQL 语法边界的手段。类型检查只是辅助,不能替代它。
容易被忽略的绕过点:URL路径和请求头中的数字参数
很多人只校验 $_GET 和 $_POST,却忘了这些位置也可能带数字型注入点:
- RESTful 路径:
/api/user/1中的1若直接拼进 SQL,一样可被替换为1%20UNION%20SELECT... - HTTP 请求头:
X-User-ID: 123若未走参数化,header 值照样能触发注入 - Cookie 值:
session_id=98765如果用于日志查询或权限校验,且拼接进 SQL,同样危险
防护边界必须覆盖所有用户可控输入源,不能只盯表单字段。一个没被参数化的数字型 Cookie 值,和一个没过滤的搜索框,危害等级完全相同。











