本文目录导读:

针对PHP项目中的越权访问漏洞(分为水平越权:同级别用户越权操作其他用户数据;垂直越权:低权限用户操作高权限功能),以下是系统的排查与修复指南。
排查步骤
代码审计:定位关键入口
重点检查 API 接口、控制器方法、后台管理文件,典型特征:
- 通过
$_GET、$_POST、$_REQUEST获取用户 ID、订单号、资源 ID 等参数 - 直接使用参数查询数据库,如
SELECT * FROM orders WHERE id = $_GET['order_id'] - 未对
$_SESSION、$_SERVER['REQUEST_URI']等来源的权限做校验
权限校验点检查
- 检查所有涉及数据读写的代码块:是否在操作前校验当前用户对目标资源的归属权/授权
- 检查角色/权限判断函数:如
isAdmin()、checkPermission()是否被遗漏或实现有漏洞 - 检查文件包含:是否存在未校验权限的后台文件直接可被访问(如
/admin/export.php未鉴权)
动态测试(黑盒+白盒)
- 手动测试:修改请求参数(URL、POST数据),模拟不同用户访问
- 水平越权测试:登录用户 A,将请求中的
user_id=123改为user_id=456 - 垂直越权测试:普通用户直接访问
/admin/user_list,观察返回
- 水平越权测试:登录用户 A,将请求中的
- 自动化工具:使用 Burp Suite 的 Authz/Auth Analyzer 插件自动对比未授权与授权请求的响应差异
常见漏洞模式
- 直接对象引用(IDOR):如
delete.php?file_id=333未验证文件归属 - 未做角色路由限制:admin 路由未在框架层面拦截,普通用户可访问
- 权限判断依赖前端:前端隐藏按钮但后端未校验
- Session 或 Token 未绑定用户:仅检查 token 未过期,未验证 token 所属用户
修复方案
建立统一权限校验层
推荐使用 中间件 或 AOP(面向切面编程),避免在每个方法中重复写校验代码。
ThinkPHP 中间件示例:
// app/middleware/AuthCheck.php
public function handle($request, \Closure $next)
{
$user = session('user');
if (!$user) {
return redirect('/login');
}
// 检查请求路径与用户角色匹配
$route = $request->pathinfo();
$permMap = [
'admin/*' => ['admin'],
'user/*' => ['user', 'admin'],
];
if (!$this->checkRoutePerm($route, $user['role'], $permMap)) {
return response('权限不足', 403);
}
return $next($request);
}
水平越权:资源归属校验
操作任何用户关联数据时,必须验证当前用户与资源所有者的关系。
正确示例(基于当前用户ID):
// 修复前(危险)
$orderId = $_GET['order_id'];
$order = DB::table('orders')->where('id', $orderId)->first();
return $order;
// 修复后(加用户ID条件)
$userId = session('user_id');
$order = DB::table('orders')
->where('id', $orderId)
->where('user_id', $userId) // 关键:限制只能查自己的
->first();
if (!$order) {
return response('订单不存在或无权限', 403);
}
复杂场景(如群组资源):
// 检查用户是否在群组成员中
$file = File::find($fileId);
if (!$file->group->users->contains('id', Auth::id())) {
abort(403, '无权访问此文件');
}
垂直越权:RBAC(基于角色的访问控制)
定义角色-权限映射表,每次执行敏感操作前校验。
数据库结构:
-- 用户角色表 CREATE TABLE roles (id INT, name VARCHAR(50)); -- 权限表 CREATE TABLE permissions (id INT, name VARCHAR(50), code VARCHAR(50)); -- 角色-权限关联表 CREATE TABLE role_permission (role_id INT, permission_id INT);
代码校验:
function checkPermission($permissionCode) {
$user = session('user');
$roleId = $user['role_id'];
$hasPerm = DB::table('role_permission')
->join('permissions', 'role_permission.permission_id', '=', 'permissions.id')
->where('role_permission.role_id', $roleId)
->where('permissions.code', $permissionCode)
->exists();
if (!$hasPerm) {
throw new Exception('权限不足');
}
}
其他关键修复点
- 禁用直接对象引用:不使用可猜测的连续ID(如1,2,3),改用UUID或哈希ID
// 生成唯一资源标识 $fileUuid = Str::uuid(); // 查询时使用 uuid 而非 id 作为参数
- 后端强制校验所有敏感操作:不要相信前端传递的任何权限标记(如
is_admin=1) - 参数化查询:防止SQL注入(但与越权无关,但需同时修复)
- 日志监控:记录异常访问行为(如某用户频繁修改他人ID参数)
- 框架全局防护:使用 Laravel 的
Gate/Policy或 ThinkPHP 的@authorize注解
快速排查清单
| 步骤 | 检查项 | 工具/方法 |
|---|---|---|
| 1 | 所有带ID参数的接口 | grep -r "\$_GET['.*id']" /app/ |
| 2 | SQL语句中是否使用当前用户ID作为条件 | 代码审查 |
| 3 | admin目录是否被Nginx正确拦截 | curl http://domain/admin/xxx --cookie "session=xxx" |
| 4 | 角色路由注册是否完整 | 检查路由文件如 routes/admin.php |
| 5 | 文件上传/删除操作 | 检查文件路径是否被篡改(如 路径遍历) |
测试验证
修复完成后进行回归测试:
- 水平越权:登录用户 tom,尝试访问
/api/order/list?user_id=jerry,应返回 403 或空结果 - 垂直越权:普通用户直接 POST 到
/api/admin/deleteUser,应被中间件拦截 - 批量越权:使用 Burp Intruder 枚举 id=1..1000 的接口,查看是否有非本人数据返回