19.11-越权访问防护
要点
- 越权访问(Broken Access Control)是 OWASP Top 10 排名第一的安全风险——用户 A 访问到了用户 B 的数据
- 两类越权:水平越权(同角色用户互相访问数据)和垂直越权(低权限用户访问高权限功能)
- 越权的根本原因:服务端没有在数据层做权限校验,只依赖前端隐藏或 URL 不可见
- 防御核心:每个数据操作都必须校验「当前用户是否有权操作这条数据」——不是「当前用户是否登录了」
- IDOR(Insecure Direct Object Reference)是最常见的越权形式——用 ID 直接引用对象,没有权限校验
- AI 场景的越权:用户通过 prompt 诱导 LLM 访问其他用户的数据、Agent 工具调用越权
1. 越权访问的分类
1.1 水平越权(Horizontal Privilege Escalation)
同角色用户之间互相访问数据:
用户 A 登录 → 访问 /api/orders/123 → 看到订单 123
用户 B 登录 → 访问 /api/orders/123 → 也看到订单 123(但这是 A 的订单)用户 A 和 B 角色相同(都是普通用户),但 A 能访问 B 的数据。
1.2 垂直越权(Vertical Privilege Escalation)
低权限用户访问高权限功能:
普通用户 → 访问 /api/admin/users → 看到所有用户列表普通用户不应该能访问管理员接口,但服务端没有做角色检查。
1.3 IDOR(Insecure Direct Object Reference)
IDOR 是水平越权最常见的形式——通过 ID 直接引用对象,没有校验引用者是否有权访问:
// 危险:只校验了登录,没有校验数据归属
app.get('/api/orders/:id', authMiddleware, async (c) => {
const order = await c.env.DB
.prepare('SELECT * FROM orders WHERE id = ?')
.bind(c.req.param('id'))
.first()
if (!order) return c.json({ error: 'Not found' }, 404)
return c.json(order) // ← 任何登录用户都能看到任何订单
})2. 防御水平越权
2.1 数据层权限校验
每个数据查询都必须加上「当前用户」的条件:
// 正确:查询时带上用户 ID
app.get('/api/orders/:id', authMiddleware, async (c) => {
const userId = c.get('user').id
const order = await c.env.DB
.prepare('SELECT * FROM orders WHERE id = ? AND user_id = ?')
.bind(c.req.param('id'), userId)
.first()
if (!order) return c.json({ error: 'Not found' }, 404)
return c.json(order)
})用户只能看到 user_id = 当前用户 ID 的订单。访问其他用户的订单会返回 404(跟不存在一样,不泄露信息)。
2.2 统一的数据访问层
把权限校验封装到数据访问层,避免每个路由都重复写:
// src/lib/data-access.ts
class DataAccess {
private db: D1Database
private userId: string
constructor(db: D1Database, userId: string) {
this.db = db
this.userId = userId
}
// 只能查自己的订单
async getOrder(orderId: string) {
return this.db
.prepare('SELECT * FROM orders WHERE id = ? AND user_id = ?')
.bind(orderId, this.userId)
.first()
}
// 只能查自己的订单列表
async listOrders(page: number, pageSize: number) {
return this.db
.prepare('SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?')
.bind(this.userId, pageSize, (page - 1) * pageSize)
.all()
}
// 只能更新自己的订单
async updateOrder(orderId: string, data: any) {
const result = await this.db
.prepare('UPDATE orders SET ? WHERE id = ? AND user_id = ?')
.bind(data, orderId, this.userId)
.run()
if (result.meta.changes === 0) {
throw new Error('Order not found or access denied')
}
}
// 只能删自己的订单
async deleteOrder(orderId: string) {
const result = await this.db
.prepare('DELETE FROM orders WHERE id = ? AND user_id = ?')
.bind(orderId, this.userId)
.run()
if (result.meta.changes === 0) {
throw new Error('Order not found or access denied')
}
}
}
// 路由中使用
app.get('/api/orders/:id', authMiddleware, async (c) => {
const da = new DataAccess(c.env.DB, c.get('user').id)
const order = await da.getOrder(c.req.param('id'))
if (!order) return c.json({ error: 'Not found' }, 404)
return c.json(order)
})2.3 批量操作的越权
批量操作也容易出越权——攻击者传入别人的 ID 列表:
// 危险:批量删除没有校验归属
app.post('/api/orders/batch-delete', authMiddleware, async (c) => {
const { ids } = await c.req.json()
for (const id of ids) {
await c.env.DB.prepare('DELETE FROM orders WHERE id = ?').bind(id).run()
// ← 任何登录用户可以删除任何订单
}
return c.json({ ok: true })
})
// 正确:批量操作也加上用户条件
app.post('/api/orders/batch-delete', authMiddleware, async (c) => {
const { ids } = await c.req.json()
const userId = c.get('user').id
for (const id of ids) {
const result = await c.env.DB
.prepare('DELETE FROM orders WHERE id = ? AND user_id = ?')
.bind(id, userId)
.run()
// 如果 changes = 0,说明不是该用户的订单,跳过
}
return c.json({ ok: true })
})2.4 分页/列表的越权
列表接口也容易出问题——攻击者改 user_id 参数查其他人的数据:
// 危险:用户 ID 从参数取
app.get('/api/orders', authMiddleware, async (c) => {
const userId = c.req.query('user_id') // ← 攻击者可以传别人的 user_id
const orders = await c.env.DB
.prepare('SELECT * FROM orders WHERE user_id = ?')
.bind(userId)
.all()
return c.json(orders.results)
})
// 正确:用户 ID 从 JWT 取
app.get('/api/orders', authMiddleware, async (c) => {
const userId = c.get('user').id // ← 从认证中间件取,不可伪造
const orders = await c.env.DB
.prepare('SELECT * FROM orders WHERE user_id = ?')
.bind(userId)
.all()
return c.json(orders.results)
})3. 防御垂直越权
3.1 角色检查中间件
// 角色检查中间件
function requireRole(...roles: string[]) {
return async (c: Context, next: Next) => {
const user = c.get('user')
if (!user) return c.json({ error: 'Unauthorized' }, 401)
if (!roles.includes(user.role)) {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
}
}
// 使用
app.get('/api/admin/users',
authMiddleware,
requireRole('admin'), // ← 只有管理员能访问
async (c) => {
const users = await c.env.DB.prepare('SELECT * FROM users').all()
return c.json(users.results)
}
)
app.get('/api/admin/stats',
authMiddleware,
requireRole('admin', 'moderator'), // 管理员或版主
async (c) => {
// ...
}
)3.2 基于权限的检查
比角色更细粒度的是基于权限的检查:
// 权限定义
const PERMISSIONS = {
'admin': ['user:read', 'user:write', 'user:delete', 'order:read', 'order:write'],
'moderator': ['user:read', 'order:read', 'order:write'],
'user': ['order:read', 'order:write'],
'viewer': ['order:read'],
}
// 权限检查中间件
function requirePermission(...permissions: string[]) {
return async (c: Context, next: Next) => {
const user = c.get('user')
const userPermissions = PERMISSIONS[user.role] || []
const hasAll = permissions.every(p => userPermissions.includes(p))
if (!hasAll) {
return c.json({ error: 'Insufficient permissions' }, 403)
}
await next()
}
}
// 使用
app.delete('/api/orders/:id',
authMiddleware,
requirePermission('order:delete'),
async (c) => { /* ... */ }
)3.3 资源所有权 + 角色检查
很多场景需要同时检查「角色」和「数据归属」:
// 用户可以编辑自己的订单,管理员可以编辑任何订单
app.put('/api/orders/:id', authMiddleware, async (c) => {
const user = c.get('user')
const orderId = c.req.param('id')
const data = await c.req.json()
// 获取订单
const order = await c.env.DB
.prepare('SELECT * FROM orders WHERE id = ?')
.bind(orderId)
.first()
if (!order) return c.json({ error: 'Not found' }, 404)
// 权限检查:是管理员 或 是订单所有者
const isAdmin = user.role === 'admin'
const isOwner = order.user_id === user.id
if (!isAdmin && !isOwner) {
return c.json({ error: 'Forbidden' }, 403)
}
// 更新订单
await c.env.DB
.prepare('UPDATE orders SET ? WHERE id = ?')
.bind(data, orderId)
.run()
return c.json({ ok: true })
})4. 常见的越权陷阱
4.1 前端隐藏不等于权限控制
// 危险:只在前端隐藏管理员按钮
// 前端:
{user.role !== 'admin' && <button>管理</button>}
// ← 攻击者直接调 API,前端隐藏不起任何作用前端隐藏只是 UI 层面的控制,不替代服务端权限校验。
4.2 HTTP 方法切换
// 只对 GET 做了权限检查
app.get('/api/orders/:id', authMiddleware, checkOwnership, ...)
// 但 PUT 没有检查
app.put('/api/orders/:id', authMiddleware, async (c) => {
// ← 没有 checkOwnership,任何登录用户可以更新任何订单
})每个 HTTP 方法都需要独立的权限检查。
4.3 文件路径越权
// 危险:用户控制的 filename 没有校验
app.get('/api/files/:filename', authMiddleware, async (c) => {
const filename = c.req.param('filename')
const file = await c.env.R2.get(`uploads/${filename}`)
// filename = "../../other-user/file.txt" → 可能访问其他用户的文件
return c.body(await file.arrayBuffer())
})
// 正确:用用户 ID 隔离路径
app.get('/api/files/:filename', authMiddleware, async (c) => {
const userId = c.get('user').id
const filename = c.req.param('filename')
const file = await c.env.R2.get(`uploads/${userId}/${filename}`)
// 每个用户只能访问自己目录下的文件
return c.body(await file.arrayBuffer())
})4.4 共享链接越权
// 危险:共享链接没有过期和权限控制
app.get('/api/shared/:token', async (c) => {
const share = await c.env.DB
.prepare('SELECT * FROM shared_links WHERE token = ?')
.bind(c.req.param('token'))
.first()
return c.json(share) // ← token 永远有效,任何人拿到 token 都能访问
})
// 正确:共享链接有过期时间和访问次数限制
app.get('/api/shared/:token', async (c) => {
const share = await c.env.DB
.prepare('SELECT * FROM shared_links WHERE token = ? AND expires_at > ? AND access_count < max_access')
.bind(c.req.param('token'), new Date().toISOString())
.first()
if (!share) return c.json({ error: 'Link expired or not found' }, 404)
// 增加访问次数
await c.env.DB
.prepare('UPDATE shared_links SET access_count = access_count + 1 WHERE token = ?')
.bind(c.req.param('token'))
.run()
return c.json(share)
})5. AI 场景的越权
5.1 Prompt 诱导跨用户数据访问
用户 A:「帮我查一下用户 B(user_id: 456)的订单」
如果 LLM 有查询工具,可能直接调用:
query_orders(user_id: 456) → 返回用户 B 的订单防御:工具函数里也要校验「当前用户是否有权访问目标用户的数据」:
// 工具定义
const tools = [
{
name: 'query_orders',
handler: async (params, context) => {
const targetUserId = params.user_id
const currentUserId = context.user.id
// 权限校验:只能查自己的订单(除非是管理员)
if (targetUserId !== currentUserId && context.user.role !== 'admin') {
throw new Error('Access denied: cannot query other users orders')
}
return context.db
.prepare('SELECT * FROM orders WHERE user_id = ?')
.bind(targetUserId)
.all()
},
},
]5.2 Agent 工具调用越权
AI Agent 可能被诱导调用不该调用的工具:
用户:「帮我删除所有订单」
Agent 调用:delete_all_orders() → 所有订单被删除防御:工具权限 + 危险操作确认:
// 不允许「删除所有」类操作
const dangerousPatterns = [
/delete.*all/i,
/drop\s+table/i,
/truncate/i,
]
async function validateToolCall(toolName: string, params: any) {
const paramStr = JSON.stringify(params)
for (const pattern of dangerousPatterns) {
if (pattern.test(paramStr)) {
throw new Error(`Dangerous operation detected: ${toolName}`)
}
}
}6. 越权防护检查清单
| 检查项 | 说明 |
|---|---|
| 每个数据查询带用户条件 | WHERE user_id = ? |
| 批量操作也校验归属 | 不只是列表查询 |
| 用户 ID 从 JWT 取不从参数取 | 参数可以伪造 |
| 所有 HTTP 方法都做权限检查 | GET/POST/PUT/DELETE 都要 |
| 管理接口有角色检查 | requireRole('admin') |
| 文件路径按用户隔离 | uploads/{userId}/filename |
| 共享链接有过期机制 | 不是永久有效 |
| AI 工具也做权限校验 | 不能因为是 LLM 调用就跳过 |
| 危险操作需要确认 | 删除、批量修改等操作 |
7. 小结
越权访问是 OWASP Top 10 排名第一的安全风险。两类越权:水平越权(同角色用户互相访问数据)和垂直越权(低权限访问高权限功能)。
防御核心:每个数据操作都必须校验当前用户是否有权操作这条数据。不能只校验「用户是否登录」,要校验「数据是否属于该用户」。
常见陷阱:前端隐藏不等于权限控制、HTTP 方法切换绕过检查、文件路径穿越、共享链接永不过期。
AI 场景的越权:Prompt 诱导跨用户数据访问、Agent 工具调用越权。工具函数里也要做权限校验——不能因为是 LLM 调用就跳过。
下一篇讲敏感信息脱敏——即使权限控制正确,输出里也可能包含不该暴露的敏感数据。