关于 access-control-patterns
access-control-patterns 是面向 Web 和多租户应用授权设计的安全技能。它结合核心原则、多个框架的实现示例及审计清单,帮助保护路由、资源和管理操作。
主要功能
- 01身份验证与授权原则
- 02按租户限制资源访问
- 03RBAC 与 ABAC 模式
- 04特定框架的中间件示例
- 05数据库行级安全指导
- 06访问控制审计清单
适用场景
- 01检查 Web 应用的授权设计
- 02防止跨租户数据泄露
- 03为受保护操作设计权限
- 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 调用管理员端点。
规则
身份验证用于证明用户是谁。授权用于证明用户可以做什么。每项受保护的操作都需要同时进行身份验证和授权。
核心原则
- 默认拒绝 -- 除非明确标记为公开,否则任何路由都不应公开
- 在服务端检查授权 -- 绝不信任隐藏按钮或客户端检查
- 按用户或租户限定每次查询的范围 -- 绝不能仅按原始 ID 获取数据
- 使用集中式策略 -- 避免散落的
if (user.role === 'admin')检查 - 将角色与权限分开 -- 角色是权限集合,权限是具体操作
- 审计敏感决策 -- 记录被拒绝的操作和特权操作
易受攻击的模式
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。
企业授权模型
对每项受保护的操作,按以下顺序进行判断:
- 用户是否已通过身份验证?
- 账户是否处于活跃状态且未被暂停?
- 用户是否属于正在访问的租户/工作区?
- 用户是否拥有所需权限?
- 资源是否属于用户所在的租户?
- 是否存在对象级条件?(所有者、被分配的用户、审批状态)
- 是否应记录该操作的审计日志?
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