Security Notice: Admin Panel RBAC Gate Disabled in v1.0.25
Security Notice: Admin Panel RBAC Gate Disabled in v1.0.25
Severity: High
Affected versions: All versions up to and including v1.0.25
Affected route:/dashboard/admin
Affected file:src/app/dashboard/admin/page.tsx
What Happened
During a code review for v1.0.25, it was identified that the role-based access control (RBAC) check on the admin panel page has been left commented out. The intended guard — which restricts access to users with the owner or admin role — is not being executed:
// const member = await getCurrentOrgMember(orgId);
// if (!member || !["owner", "admin"].includes(member.role)) redirect("/dashboard");
As a result, any authenticated user in your organisation can navigate to /dashboard/admin and view the batch jobs panel and system statistics, regardless of their assigned role.
Who Is Affected
This affects all multi-tenant deployments where:
- Users with roles other than
owneroradmin(e.g. standard members, learners) have active accounts. - The admin panel exposes sensitive operational data such as batch import job status or platform-wide system stats.
Risk
In a B2B multi-tenant SaaS context, the admin panel is intended exclusively for organisation owners and administrators. Without the RBAC gate:
- Regular members can view batch job queues and system statistics they should not have access to.
- There is a potential for information disclosure across tenant operational boundaries.
- No write-access or destructive actions have been confirmed as exposed, but the surface area should be treated as sensitive.
Recommended Remediation
Uncomment and update the RBAC check in src/app/dashboard/admin/page.tsx. The x-org-id cookie is already set by the middleware layer, so the fix reads the organisation context from cookies, queries the orgMembers table, and redirects non-admins:
import { cookies } from 'next/headers';
import { db } from '@/lib/db';
import { orgMembers } from '@/lib/db/schema';
import { and, eq } from 'drizzle-orm';
import { redirect } from 'next/navigation';
// Inside your async page component, after session validation:
const orgId = (await cookies()).get('x-org-id')?.value;
if (orgId) {
const [m] = await db
.select()
.from(orgMembers)
.where(
and(
eq(orgMembers.orgId, orgId),
eq(orgMembers.userId, session.user.id)
)
);
if (!m || !['owner', 'admin'].includes(m.role)) {
redirect('/dashboard');
}
}
What This Fix Does
- Reads
orgIdfrom thex-org-idcookie, which is reliably set by the existing middleware on every authenticated request. - Queries
orgMembersto find the current user's membership record for that organisation. - Checks the role — if the user is not an
owneroradmin, or if no membership record is found, they are immediately redirected to/dashboard. - Allows access only for confirmed
ownerandadminrole holders.
Verification
After applying the fix, verify the following:
- A user with role
memberorlearneris redirected to/dashboardwhen attempting to access/dashboard/admin. - A user with role
ownercan access/dashboard/adminwithout redirection. - A user with role
admincan access/dashboard/adminwithout redirection. - An unauthenticated user is redirected to the login page (existing session middleware behaviour).
Background: RBAC in the Platform
This platform uses an organisation-scoped membership model. Each user belongs to one or more organisations and holds a role within each (owner, admin, or member). The x-org-id cookie identifies the active organisation context for a given session and is set automatically by request middleware — it does not need to be passed manually through page props.
The orgMembers table is the authoritative source for role lookups and should be queried server-side on any route that exposes privileged functionality.
This notice was published as part of the v1.0.25 release. Apply the remediation above as soon as possible and redeploy.