越权漏洞全解
越权(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();
}
七、总结
越权漏洞的核心就一句话:谁能对什么对象做什么操作,这三件事必须同时校验。记住这个公式:
安全的接口 = 认证(你是谁) + 角色检查(你能做这类操作吗) + 归属检查(这个东西是你的吗)
三者缺一不可。大多数越权漏洞都是因为漏掉了第三条——归属检查。