ThinkPHP的Validate无法校验嵌套路径存在性,需在验证前用filterRequestData按白名单路径逐层提取合法数据,避免非法字段混入;同时须关闭param_merge、对齐白名单与验证规则、补充类型校验。

ThinkPHP 的 validate 无法直接校验嵌套路径存在性
ThinkPHP 原生的验证器(Validate)只校验字段值是否符合规则,不校验「请求中是否只包含白名单路径」。比如你只允许 user.name 和 user.email,但用户传了 user.password 或 admin.id,默认不会报错——它压根不检查字段“是否存在”,只检查“存在时是否合法”。
所以不能靠 rule 或 scene 解决路径白名单问题,得在参数进入验证器前做结构过滤。
用 filterRequestData 手动提取白名单路径
核心思路:把原始 input() 数据当树,按白名单路径逐层取值,丢弃所有未声明的键。不是“禁止非法字段”,而是“只保留合法路径”。
- 白名单定义为二维数组,如
[['user', 'name'], ['user', 'email'], ['status']] - 遍历每条路径,在原始数据中用
array_key_exists+ 递归下标访问取值,遇到中间键不存在就跳过 - 避免用
isset($data[$k]),它对null值返回 false,而空字符串/0 是合法值 - 不要用
array_filter直接删键——它不处理嵌套,且会重排索引,破坏结构
示例片段:
立即学习“PHP免费学习笔记(深入)”;
function filterRequestData(array $data, array $whitelist): array
{
$result = [];
foreach ($whitelist as $path) {
$val = $data;
$found = true;
foreach ($path as $key) {
if (!is_array($val) || !array_key_exists($key, $val)) {
$found = false;
break;
}
$val = $val[$key];
}
if ($found) {
// 按路径写回 result,支持多层重建
$ref = &$result;
foreach (array_slice($path, 0, -1) as $k) {
if (!isset($ref[$k]) || !is_array($ref[$k])) {
$ref[$k] = [];
}
$ref = &$ref[$k];
}
$ref[end($path)] = $val;
}
}
return $result;
}
input() 默认递归合并,要防深层污染
ThinkPHP 的 input() 在开启 param_merge(默认开)时,会把 GET、POST、PUT 等自动合并,且对同名嵌套键做递归覆盖。比如 POST[user][name] 和 GET[user][password] 同时存在,最终 input('user/a') 可能混入意外字段。
- 生产环境务必关闭自动合并:
config(['app.param_merge' => false]) - 统一用
input('post.') + input('get.')显式指定来源,再进白名单过滤 - 特别注意
application/route.php中的变量绑定(如:id)也会注入到input(),需在路由层或控制器开头就做剥离
白名单路径和验证规则必须严格对齐
很多人把白名单和验证规则分开维护,结果加了一个新字段到白名单,却忘了加验证规则,或者改了规则但漏更新白名单——这会导致“能传进来但不校验”,等于裸奔。
- 建议把白名单路径和验证规则定义在同一配置文件,例如
config/request_whitelist.php - 用键名复用:如
'user.name' => 'require|alphaNum',解析时自动拆成路径数组和规则字符串 - 在控制器基类里统一调用
filterRequestData()+Validate::check(),避免每个方法重复写 - 注意
scene场景切换时,白名单也得动态切,否则create场景放行password,update场景却不该放行,但白名单没分场景就会失效
最易被忽略的是:白名单只管“路径存在”,不管“值类型”。比如允许 user.id,但没限制它必须是整数,攻击者仍可传字符串触发 SQL 隐式转换。白名单是第一道门,后面还得接类型断言和验证规则——两步缺一不可。



















