PHP项目水平越权如何代码拦截控制

wen PHP项目 29

本文目录导读:

PHP项目水平越权如何代码拦截控制

  1. 方案一:AOP(面向切面编程)或中间件拦截(最推荐)
  2. 方案二:数据层强制注入(防漏网之鱼)
  3. 方案三:Token/签名校验(适用于无状态API)
  4. 方案四:统一基础控制器(BaseController)模式
  5. 最容易忽视的3个漏洞点
  6. 生产环境最佳实践

针对PHP项目的水平越权(Horizontal Privilege Escalation)问题,核心控制策略是在所有涉及资源访问的业务逻辑层进行归属权校验,确保当前用户只能操作属于自己的数据。

以下是几种标准的代码拦截与控制实践方案,按推荐程度排序:

AOP(面向切面编程)或中间件拦截(最推荐)

这是最优雅、入侵性最低的方法,通过框架的中间件或自定义注解,在控制器执行前自动完成权限校验。

实现原理: 定义规则:当前用户ID 必须等于 资源所属用户ID

示例代码(ThinkPHP/Laravel 风格):

<?php
// 1. 定义一个资源所有者检查中间件或函数
function checkOwnership($resourceType, $resourceId) {
    $currentUserId = auth()->id(); // 当前登录用户ID
    $ownerId = getResourceOwnerId($resourceType, $resourceId); // 查询该资源属于谁
    if ($currentUserId !== $ownerId) {
        // 拦截:抛出异常或返回403
        throw new \App\Exceptions\UnauthorizedException('您无权访问该资源');
    }
}
// 2. 在控制器中调用
public function editOrder($orderId) {
    // 执行检查(这一步可放在BaseController的__construct或构造方法中自动执行)
    checkOwnership('order', $orderId);
    // ... 后续业务逻辑
}

框架集成(以 Laravel 为例):

  • 创建 Policy (策略类):php artisan make:policy OrderPolicy
  • OrderPolicy 中定义 viewupdate 等方法:
    public function view(User $user, Order $order) {
        return $user->id === $order->user_id;
    }
  • 在控制器中使用 $this->authorize('view', $order) 即可自动拦截。

数据层强制注入(防漏网之鱼)

为了防止开发人员忘记调用校验函数,建议在SQL查询时直接强制绑定当前用户ID

核心思想: 所有查询都包含 WHERE user_id = 当前用户ID,即使被绕过校验,也拿不到别人的数据。

<?php
// Repository或Model层
public function getMyOrder($orderId) {
    $currentUserId = session('user_id');
    return DB::table('orders')
        ->where('id', $orderId)
        ->where('user_id', $currentUserId)  // 强制条件
        ->first();
}
// 控制器调用
$order = $this->orderRepository->getMyOrder($inputId);
if (!$order) {
    // 要么不存在,要么不属于你,统一报404或403
    abort(403);
}

Token/签名校验(适用于无状态API)

对于前后端分离或API接口,仅靠ID不够安全,需要结合数据签名

原理: 客户端请求资源时,携带一个由用户ID + 资源ID + 密钥生成的签名,服务端验证签名,防止篡改。

<?php
// 服务端生成签名(仅示例,实际使用HMAC-SHA256)
function generateSignature($userId, $resourceId, $secretKey) {
    return hash_hmac('sha256', $userId . '|' . $resourceId, $secretKey);
}
// 客户端请求时携带 signature
// POST /api/order/update?orderId=123&signature=xxxx
// 服务端验证
$sign = $request->input('signature');
$expectedSign = generateSignature(auth()->id(), $orderId, config('app.secret'));
if (!hash_equals($expectedSign, $sign)) {
    abort(403, '签名验证失败,可能的水平越权攻击');
}

注意: 这种方式虽能防止用户修改orderId值为其他人的ID,但前端源码中会暴露签名生成逻辑,密钥不应暴露,实际项目中更建议使用JWTOAuth中携带用户身份,结合方案一使用。


统一基础控制器(BaseController)模式

如果团队不使用中间件,最原始的防止遗漏的办法是:所有需要权限校验的方法,都调用父类的一个方法。

<?php
class BaseController {
    protected function verifyAccess($model, $userIdField = 'user_id') {
        $currentUserId = session('user_id');
        $ownerId = $model->$userIdField;
        if ((int)$currentUserId !== (int)$ownerId) {
            $this->responseError('无权限', 403);
        }
    }
}
class OrderController extends BaseController {
    public function detail($id) {
        $order = OrderModel::find($id);
        $this->verifyAccess($order); // 一行代码完成校验
        // ... 业务逻辑
    }
}

最容易忽视的3个漏洞点

  1. ID类型/大小写不一致

    • $userIdint(1),但 SQL 中 user_idstring("1"),用 比较通过,严格 可能失败,反之亦然。
    • 解决: 统一做 (int) 强制转换比较。
  2. 批量操作/列表接口

    • 虽然详情页校验了,但/api/orders?page=2返回了所有用户的订单。
    • 解决: 列表查询时,必须过滤 ->where('user_id', $currentUserId)
  3. 关联查询/软删除

    • 通过查询别人的订单ID,然后利用关联的Logs表查到了不属于自己的日志。
    • 解决: 关联关系查询也要带 ->where('user_id', $currentUserId)

生产环境最佳实践

  1. 基础层: 在数据库查询层(Repository/Model)强制注入 user_id(方案二)。
  2. 拦截层: 使用框架中间件或Policy(方案一),拦截80%的越权行为。
  3. 审计层: 日志记录所有ID参数与user_id的对应关系,出现异常告警。
  4. 兜底: 统一异常处理,返回 403404(避免泄露“存在但无权”的信息,用404更安全)。

一句话口诀:

增删改查都要验,不查ID查自己;资源归属不对等,统一返回403。

抱歉,评论功能暂时关闭!