mcpField
A
access-control-patterns

access-control-patterns

github.com/afu-it安全3444 次安装22 周前更新
代码仓库

关于 access-control-patterns

access-control-patterns 是面向 Web 和多租户应用授权设计的安全技能。它结合核心原则、多个框架的实现示例及审计清单,帮助保护路由、资源和管理操作。

主要功能

  1. 01身份验证与授权原则
  2. 02按租户限制资源访问
  3. 03RBAC 与 ABAC 模式
  4. 04特定框架的中间件示例
  5. 05数据库行级安全指导
  6. 06访问控制审计清单

适用场景

  1. 01检查 Web 应用的授权设计
  2. 02防止跨租户数据泄露
  3. 03为受保护操作设计权限
  4. 04审计管理路由与访问控制

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/afu-it/security-for-vibecoders --skill access-control-patterns

安装后在 Claude Code 中输入/access-control-patterns确认已加载。

详细介绍

访问控制模式

防止用户访问其无权访问的数据或执行其无权执行的操作。访问控制失效是 OWASP A01,因为它是最常见的严重 Web 漏洞:用户 A 可以读取用户 B 的发票、更改其他租户的设置,或通过猜测 ID 调用管理员端点。

规则

身份验证用于证明用户是谁。授权用于证明用户可以做什么。每项受保护的操作都需要同时进行身份验证和授权。

核心原则

  1. 默认拒绝 -- 除非明确标记为公开,否则任何路由都不应公开
  2. 在服务端检查授权 -- 绝不信任隐藏按钮或客户端检查
  3. 按用户或租户限定每次查询的范围 -- 绝不能仅按原始 ID 获取数据
  4. 使用集中式策略 -- 避免散落的 if (user.role === 'admin') 检查
  5. 将角色与权限分开 -- 角色是权限集合,权限是具体操作
  6. 审计敏感决策 -- 记录被拒绝的操作和特权操作

易受攻击的模式

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

错误:

app.get('/api/invoices/:id', async (req, res) => {
  const invoice = await db.invoice.findUnique({ where: { id: req.params.id } });
  res.json(invoice); // Any logged-in user can read any invoice by guessing ID
});

正确:

app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.invoice.findFirst({
    where: {
      id: req.params.id,
      tenantId: req.user.tenantId,
    },
  });

  if (!invoice) return res.status(404).json({ error: 'Not found' });
  res.json(invoice);
});

如果不应透露资源是否存在,请返回 404,而不是 403。

企业授权模型

对每项受保护的操作,按以下顺序进行判断:

  1. 用户是否已通过身份验证?
  2. 账户是否处于活跃状态且未被暂停?
  3. 用户是否属于正在访问的租户/工作区?
  4. 用户是否拥有所需权限?
  5. 资源是否属于用户所在的租户?
  6. 是否存在对象级条件?(所有者、被分配的用户、审批状态)
  7. 是否应记录该操作的审计日志?

RBAC 模式

type Role = 'owner' | 'admin' | 'member' | 'viewer';
type Permission =
  | 'invoice:read'
  | 'invoice:create'
  | 'invoice:update'
  | 'invoice:delete'
  | 'user:invite'
  | 'billing:manage';

const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
  owner: ['invoice:read', 'invoice:create', 'invoice:update', 'invoice:delete', 'user:invite', 'billing:manage'],
  admin: ['invoice:read', 'invoice:create', 'invoice:update', 'user:invite'],
  member: ['invoice:read', 'invoice:create'],
  viewer: ['invoice:read'],
};

function hasPermission(role: Role, permission: Permission): boolean {
  return ROLE_PERMISSIONS[role]?.includes(permission) ?? false;
}

ABAC 模式

基于属性的访问控制会增加对象级条件。

interface AuthContext {
  userId: string;
  tenantId: string;
  role: Role;
  permissions: Permission[];
}

interface Invoice {
  id: string;
  tenantId: string;
  ownerId: string;
  status: 'draft' | 'sent' | 'paid';
}

function canUpdateInvoice(user: AuthContext, invoice: Invoice): boolean {
  if (invoice.tenantId !== user.tenantId) return false;
  if (!hasPermission(user.role, 'invoice:update')) return false;
  if (invoice.status === 'paid') return false;
  if (user.role === 'member' && invoice.ownerId !== user.userId) return false;
  return true;
}

Node.js / Express 中间件

function requireAuth(req: Request, res: Response, next: NextFunction) {
  if (!req.user) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  next();
}

function requirePermission(permission: Permission) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    if (!hasPermission(req.user.role, permission)) {
      auditLog({
        event: 'access.denied',
        userId: req.user.id,
        tenantId: req.user.tenantId,
        permission,
        path: req.path,
      });

      return res.status(403).json({ error: 'Forbidden' });
    }

    next();
  };
}

app.patch('/api/invoices/:id', requirePermission('invoice:update'), async (req, res) => {
  const invoice = await db.invoice.findFirst({
    where: { id: req.params.id, tenantId: req.user.tenantId },
  });

  if (!invoice) return res.status(404).json({ error: 'Not found' });
  if (!canUpdateInvoice(req.user, invoice)) return res.status(403).json({ error: 'Forbidden' });

  const updated = await db.invoice.update({
    where: { id: invoice.id },
    data: req.body,
  });

  res.json(updated);
});

Cloudflare Workers 模式

interface AuthContext {
  userId: string;
  tenantId: string;
  role: Role;
}

async function requireAuth(request: Request, env: Env): Promise<AuthContext> {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  if (!token) throw new HttpError(401, 'Authentication required');

  const session = await verifySession(token, env);
  if (!session) throw new HttpError(401, 'Invalid session');

  return session;
}

function requirePermission(auth: AuthContext, permission: Permission): void {
  if (!hasPermission(auth.role, permission)) {
    throw new HttpError(403, 'Forbidden');
  }
}

async function getTenantScopedInvoice(db: D1Database, auth: AuthContext, invoiceId: string) {
  return db.prepare(
    'SELECT * FROM invoices WHERE id = ? AND tenant_id = ?'
  ).bind(invoiceId, auth.tenantId).first();
}

Laravel 策略模式

// app/Policies/InvoicePolicy.php
class InvoicePolicy
{
    public function view(User $user, Invoice $invoice): bool
    {
        return $user->tenant_id === $invoice->tenant_id
            && $user->hasPermission('invoice:read');
    }

    public function update(User $user, Invoice $invoice): bool
    {
        return $user->tenant_id === $invoice->tenant_id
            && $user->hasPermission('invoice:update')
            && $invoice->status !== 'paid';
    }
}

// Controller
public function update(Request $request, Invoice $invoice)
{
    $this->authorize('update', $invoice);
    $invoice->update($request->validated());
    return response()->json($invoice);
}

始终为路由模型绑定或全局作用域添加租户范围限制:

protected static function booted()
{
    static::addGlobalScope('tenant', function (Builder $builder) {
        if (auth()->check()) {
            $builder->where('tenant_id', auth()->user()->tenant_id);
        }
    });
}

Python FastAPI 模式

from fastapi import Depends, HTTPException

def require_permission(permission: str):
    def dependency(user: User = Depends(current_user)):
        if permission not in user.permissions:
            raise HTTPException(status_code=403, detail="Forbidden")
        return user
    return dependency

@app.patch("/invoices/{invoice_id}")
async def update_invoice(
    invoice_id: str,
    payload: InvoiceUpdate,
    user: User = Depends(require_permission("invoice:update")),
):
    invoice = await db.fetch_one(
        "SELECT * FROM invoices WHERE id = :id AND tenant_id = :tenant_id",
        {"id": invoice_id, "tenant_id": user.tenant_id},
    )
    if not invoice:
        raise HTTPException(status_code=404, detail="Not found")

    if invoice["status"] == "paid":
        raise HTTPException(status_code=403, detail="Forbidden")

    return await update_invoice_record(invoice_id, payload)

数据库行级安全

对于 PostgreSQL,还应在数据库层强制实施租户隔离。

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON invoices
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Set this per request/transaction
SELECT set_config('app.current_tenant_id', 'tenant-uuid-here', true);

RLS 不能取代应用授权,但当开发者忘记添加租户筛选条件时,它能缩小影响范围。

管理员路由保护

管理员路由需要更严格的控制:

  • 将 admin:* 权限与用户角色分开
  • 管理员操作要求使用 MFA
  • 敏感更改要求近期通过身份验证
  • 对所有管理员读取和写入操作记录审计日志
  • 绝不依赖前端隐藏路由
  • 尽可能阻止来自未知 IP 的管理员访问
function requireAdminAction(action: Permission) {
  return [
    requireAuth,
    requirePermission(action),
    requireRecentMfa({ maxAgeMinutes: 15 }),
    auditAdminAction(action),
  ];
}

访问控制审计清单

  • 每条受保护的路由都使用身份验证中间件
  • 每项敏感操作都进行权限检查
  • 每次资源查询都按 tenantId 或 userId 限定范围
  • 不使用原始 ID 查找属于租户的资源
  • 用户无法通过更改 URL 中的 ID 访问其他租户的数据
  • 前端检查也在服务端执行
  • 管理员路由要求明确的管理员权限
  • 管理员操作要求 MFA 或近期重新进行身份验证
  • 被拒绝的访问尝试均记录审计日志
  • 未经授权时不会泄露资源是否存在
  • 角色和权限均在集中位置定义
  • 多租户应用具备数据库 RLS 或同等防护措施
  • 测试涵盖跨租户访问尝试

必需的测试

每个多租户项目都必须包含以下测试:

it('prevents user from reading another tenant invoice', async () => {
  const tenantAInvoice = await createInvoice({ tenantId: 'tenant-a' });
  const tenantBUser = await createUser({ tenantId: 'tenant-b' });

  const response = await request(app)
    .get(`/api/invoices/${tenantAInvoice.id}`)
    .set('Authorization', `Bearer ${tokenFor(tenantBUser)}`);

  expect(response.status).toBe(404);
});

PDPA 2024 后果

访问控制失效是造成必须报告的个人数据泄露最快的途径。如果一位客户能够访问另一位客户的个人数据,就说明开发者未能实施基本的授权和租户隔离。根据 PDPA 2024 Seksyen 5(1) 和 5(2),这可能导致最高 RM1,000,000 罚款或 3 年监禁。

评论

支持 Markdown