越权漏洞全解

越权(Authorization Bypass)的本质是:应用只验证了"你是谁"(认证 Authentication),却没有验证"你能做什么"(授权 Authorization)

一、三种越权类型

┌─────────────────────────────────────────────────────────┐
│                      越权分类                            │
├──────────────┬──────────────────┬──────────────────────┤
│   IDOR       │   水平越权        │    垂直越权          │
│  (对象引用) │ (同权限不同用户) │   (低权限→高权限)  │
├──────────────┼──────────────────┼──────────────────────┤
│ 访问任意对象 │ A用户访问B用户数据 │ 普通用户当管理员用    │
└──────────────┴──────────────────┴──────────────────────┘

二、IDOR:不安全的直接对象引用

2.1 漏洞描述

开发者直接使用对象 ID(如 ?userId=123)作为查询条件,但没有校验当前用户是否有权访问该对象。

2.2 漏洞代码

// VULNERABLE: 直接用 URL 参数查询,没有归属校验
app.get('/api/orders/detail', authMiddleware, async (req, res) => {
  const { orderId } = req.query;

  // ❌ 没有检查这个订单是不是当前用户的
  const order = await db.Order.findOne({ where: { id: orderId } });
  if (!order) return res.status(404).json({ error: '订单不存在' });

  // 攻击者只要改 orderId 就能看到别人的订单
  res.json(order);
});

2.3 攻击演示

# 正常请求:查看自己的订单
GET /api/orders/detail?orderId=1001
Authorization: Bearer USER_A_TOKEN

# 越权请求:改 orderId 就能看 B 的订单
GET /api/orders/detail?orderId=1002
Authorization: Bearer USER_A_TOKEN

2.4 修复方案

// FIXED: 查询条件必须带上 userId
app.get('/api/orders/detail', authMiddleware, async (req, res) => {
  const { orderId } = req.query;

  // ✅ 同时按 orderId 和 userId 查询
  const order = await db.Order.findOne({
    where: { id: orderId, userId: req.user.id },
  });
  if (!order) {
    // ✅ 用 404 而不是 403,防止攻击者枚举订单 ID
    return res.status(404).json({ error: '订单不存在' });
  }

  res.json(order);
});

三、水平越权

3.1 漏洞描述

同一权限级别的用户 A 访问了用户 B 的资源。与 IDOR 本质相同,但更强调业务场景。

3.2 案例一:用户资料修改

// VULNERABLE
app.put('/api/user/profile', authMiddleware, async (req, res) => {
  const { userId, nickname } = req.body;
  // ❌ 允许修改任意 userId 的资料
  await db.User.update({ nickname }, { where: { id: userId } });
  res.json({ success: true });
});

// FIXED
app.put('/api/user/profile', authMiddleware, async (req, res) => {
  const { nickname } = req.body; // ✅ 不接受外部 userId
  await db.User.update({ nickname }, { where: { id: req.user.id } });
  res.json({ success: true });
});

3.3 案例二:收货地址

// FIXED: 查询条件强制绑定 userId
app.get('/api/address/list', authMiddleware, async (req, res) => {
  const addresses = await db.Address.findAll({
    where: { userId: req.user.id }, // ✅ 必须加
    attributes: ['id', 'receiver', 'phone', 'detail'],
  });
  res.json(addresses);
});

四、垂直越权

4.1 漏洞描述

低权限用户访问了高权限用户(通常是管理员)的功能。

4.2 漏洞代码:缺少角色校验

// VULNERABLE: 只做了登录校验,没做角色校验
app.post('/api/admin/user/delete', authMiddleware, async (req, res) => {
  const { targetUserId } = req.body;

  // ❌ 没有检查 req.user.role === 'ADMIN'
  await db.User.destroy({ where: { id: targetUserId } });
  res.json({ success: true });
});

4.3 漏洞代码:前端隐藏 ≠ 后端防护

很多应用以为前端隐藏了管理员按钮就安全了:

// 前端代码(毫无用处的"防护")
function AdminPanel() {
  const user = useUser();
  if (user.role !== 'ADMIN') return null; // 前端隐藏按钮
  return <AdminButton />;
}

// 但后端路由没有校验——攻击者直接 POST 就能调用!
app.post('/api/admin/export', async (req, res) => {
  const { startDate, endDate } = req.body;
  const data = await db.Order.findAll({ where: { createdAt: { $between: [startDate, endDate] } } });
  res.json(data); // 导出全部订单数据!
});

4.4 修复方案:RBAC 权限中间件

// FIXED: 基于角色的访问控制中间件
function requireRole(...roles) {
  return async (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: '请先登录' });
    }
    if (!roles.includes(req.user.role)) {
      // ✅ 返回 403,明确告知无权限
      return res.status(403).json({ error: '权限不足' });
    }
    next();
  };
}

// 使用示例
app.post('/api/admin/user/delete',
  authMiddleware,
  requireRole('ADMIN', 'SUPER_ADMIN'), // ✅ 只有管理员能删用户
  async (req, res) => {
    const { targetUserId } = req.body;

    // 额外安全:不能删除自己,不能删除超级管理员
    if (targetUserId === req.user.id) {
      return res.status(400).json({ error: '不能删除自己' });
    }
    const target = await db.User.findOne({ where: { id: targetUserId } });
    if (target?.role === 'SUPER_ADMIN') {
      return res.status(403).json({ error: '不能删除超级管理员' });
    }

    await target.destroy();
    res.json({ success: true });
  }
);

// 也可以用装饰器/注解方式(TypeScript 示例)
@RequireRole('ADMIN')
@Post('/api/admin/export')
async exportOrders(@Body() dto: ExportDto) {
  return this.orderService.export(dto);
}

五、高级越权场景

5.1 订单状态流转越权

// VULNERABLE: 用户可以取消别人的订单
app.post('/api/order/cancel', authMiddleware, async (req, res) => {
  const { orderId, reason } = req.body;
  await db.Order.update(
    { status: 'CANCELLED', cancelReason: reason },
    { where: { id: orderId, status: { $in: ['PENDING', 'SHIPPED'] } } }
  );
  // ❌ 没有 userId 校验
  res.json({ success: true });
});

// FIXED
app.post('/api/order/cancel', authMiddleware, async (req, res) => {
  const { orderId, reason } = req.body;
  const result = await db.Order.update(
    { status: 'CANCELLED', cancelReason: reason },
    {
      where: {
        id: orderId,
        userId: req.user.id, // ✅ 必须是自己的订单
        status: { $in: ['PENDING', 'SHIPPED'] },
      },
    }
  );
  if (result[0] === 0) {
    return res.status(404).json({ error: '订单不存在或不可取消' });
  }
  res.json({ success: true });
});

5.2 批量操作越权

// VULNERABLE: 批量操作没有逐条校验归属
app.post('/api/message/batch-delete', authMiddleware, async (req, res) => {
  const { messageIds } = req.body;
  await db.Message.destroy({ where: { id: { $in: messageIds } } });
  // ❌ messageIds 里可能包含别人的消息
  res.json({ success: true });
});

// FIXED
app.post('/api/message/batch-delete', authMiddleware, async (req, res) => {
  const { messageIds } = req.body;
  const result = await db.Message.destroy({
    where: {
      id: { $in: messageIds },
      userId: req.user.id, // ✅ 只删除自己的
    },
  });
  res.json({ deleted: result });
});

5.3 GraphQL 越权

GraphQL 的灵活查询也容易引入越权:

// VULNERABLE: GraphQL resolver 没有权限校验
const resolvers = {
  Query: {
    user: (_, { id }) => db.User.findById(id), // ❌ 可以查任何人
    order: (_, { id }) => db.Order.findById(id), // ❌ 同上
  },
};

// FIXED
const resolvers = {
  Query: {
    order: async (_, { id }, context) => {
      // ✅ GraphQL 同样需要权限校验
      const order = await db.Order.findById(id);
      if (!order || order.userId !== context.user.id) {
        throw new Error('订单不存在');
      }
      return order;
    },
  },
};

六、防御清单

原则 实现方式
默认拒绝 所有接口默认需要认证,显式标记公开接口
对象级校验 每条涉及业务对象的查询,必须校验归属关系
角色校验 管理类接口强制 RBAC 校验
禁止信任 永远不信任前端传来的 userId、ownerId 等归属字段
统一拦截 用 AOP/中间件集中做权限校验,而不是散落在业务代码中
错误模糊 对越权请求返回 404 而非 403,防止对象枚举

统一权限校验中间件示例

// 自动从路由配置生成权限检查
const routePermissions = {
  '/api/orders/detail': { roles: ['USER'], ownerParam: 'orderId', ownerField: 'userId' },
  '/api/admin/users': { roles: ['ADMIN'] },
  '/api/wallet/transfer': { roles: ['USER'] },
};

async function authorizationMiddleware(req, res, next) {
  const config = routePermissions[req.path];
  if (!config) return next(); // 无配置的公开接口

  // 角色检查
  if (config.roles && !config.roles.includes(req.user.role)) {
    return res.status(403).json({ error: '权限不足' });
  }

  // 对象归属检查
  if (config.ownerParam && config.ownerField) {
    const ownerId = req.query[config.ownerParam] || req.body[config.ownerParam];
    if (ownerId) {
      const obj = await db[getModel(req.path)].findById(ownerId);
      if (!obj || obj[config.ownerField] !== req.user.id) {
        return res.status(404).json({ error: '资源不存在' });
      }
    }
  }

  next();
}

七、总结

越权漏洞的核心就一句话:谁能对什么对象做什么操作,这三件事必须同时校验。记住这个公式:

安全的接口 = 认证(你是谁) + 角色检查(你能做这类操作吗) + 归属检查(这个东西是你的吗)

三者缺一不可。大多数越权漏洞都是因为漏掉了第三条——归属检查。