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/&#123;userId&#125;/filename
共享链接有过期机制不是永久有效
AI 工具也做权限校验不能因为是 LLM 调用就跳过
危险操作需要确认删除、批量修改等操作

7. 小结

越权访问是 OWASP Top 10 排名第一的安全风险。两类越权:水平越权(同角色用户互相访问数据)和垂直越权(低权限访问高权限功能)。

防御核心:每个数据操作都必须校验当前用户是否有权操作这条数据。不能只校验「用户是否登录」,要校验「数据是否属于该用户」。

常见陷阱:前端隐藏不等于权限控制、HTTP 方法切换绕过检查、文件路径穿越、共享链接永不过期。

AI 场景的越权:Prompt 诱导跨用户数据访问、Agent 工具调用越权。工具函数里也要做权限校验——不能因为是 LLM 调用就跳过。

下一篇讲敏感信息脱敏——即使权限控制正确,输出里也可能包含不该暴露的敏感数据。