Shared and team resources introduce a second authorization dimension: not just "is the user authenticated?" but "is the user a member of the group that owns this resource?" Skipping the membership check (CWE-639, CWE-284) means any authenticated user who knows a team project ID can read or modify it. OWASP A01 (Broken Access Control) explicitly covers this: authorization must verify the relationship between the principal and the resource, not just that a session exists. NIST 800-53 AC-3 requires access enforcement to reflect actual entitlements — membership in the owning group is the entitlement for shared resources.
High because the attack surface is every authenticated user in the system, not just attackers who have compromised specific accounts.
Before serving any shared resource, verify the requesting user is a member of the group that owns it. Do this as a database query, not as a client-supplied parameter.
// app/api/teams/[teamId]/projects/route.ts
const membership = await db.teamMember.findFirst({
where: { teamId: params.teamId, userId: session.user.id },
});
if (!membership) return NextResponse.json({ error: 'Not found' }, { status: 404 });
// Membership confirmed — safe to return team data
const projects = await db.project.findMany({ where: { teamId: params.teamId } });
Separating the membership check from the data query (rather than using a join) makes the authorization step explicit and resistant to accidental removal in future refactors.
ID: saas-authorization.resource-auth.shared-resources-access-controls
Severity: high
What to look for: Count all relevant instances and enumerate each. Look for "shared" or "team" concepts in the data model — shared documents, team projects, workspace resources, or any resource accessible by multiple users. When a route serves a shared resource, does it verify that the requesting user is a member of the group that owns the resource? Check for membership table queries (e.g., teamMembers, projectAccess, workspaceMembers) before returning shared data.
Pass criteria: Routes that serve shared resources explicitly verify the requesting user's membership in the owning group before returning data, either through a membership table query or through a relationship verified in the database query itself. At least 1 implementation must be verified.
Fail criteria: Shared resources are accessible to any authenticated user who knows (or can guess) the resource ID, with no check that the user is a member of the group that owns the resource.
Skip (N/A) when: The application is single-user only and has no shared or collaborative resource concept.
Detail on fail: "Shared resource endpoint does not verify group membership before returning data. Any authenticated user can access shared resources by ID." (Note the specific routes.)
Remediation: Verify membership in the group that owns the resource before granting access.
// app/api/teams/[teamId]/projects/route.ts
const membership = await db.teamMember.findFirst({
where: { teamId: params.teamId, userId: session.user.id },
});
if (!membership) return NextResponse.json({ error: 'Not found' }, { status: 404 });
// Now safe to return team projects
const projects = await db.project.findMany({ where: { teamId: params.teamId } });
Using findFirst for the membership check followed by an early return is the correct pattern — it avoids a join that could be accidentally omitted in future refactors.