Hiding UI elements from free users while leaving the underlying API endpoint open is security theater. Any user with browser dev tools can call the API directly, bypassing the paywall entirely. OWASP A01 (Broken Access Control) and CWE-284 (Improper Access Control) cover this exactly. Every premium feature needs enforcement at both layers: the UI gate for user experience, the API gate as the actual security boundary. NIST AC-3 (Access Enforcement) requires that access controls are applied at the resource, not just the interface.
High because UI-only gating leaves every premium API endpoint exploitable by any authenticated user with a basic HTTP client.
Add server-side subscription checks to every API route that serves premium data or actions. The UI gate controls display; the API gate is the real enforcement.
// app/api/reports/export/route.ts
export async function GET(req: Request) {
const session = await getServerSession()
const user = await db.user.findUnique({
where: { id: session?.userId },
select: { plan: true, subscription_status: true }
})
if (!user || user.plan !== 'pro' || user.subscription_status !== 'active') {
return Response.json({ error: 'Upgrade required' }, { status: 402 })
}
// serve premium content
}
For each premium feature, verify both: the UI renders an upgrade prompt for free users, AND the API returns 402 for unauthenticated or free-tier requests.
ID: saas-billing.subscription-mgmt.feature-access-gated
Severity: high
What to look for: Identify the premium or paid features in the application (look for references to plans, tiers, or feature flags in route files, component files, and API routes). For each premium feature, verify that access is controlled by a server-side check that reads subscription status. Look for API routes that serve premium data, actions, or capabilities without checking subscription. Also look for UI-only gating (feature visible in UI to paid users only) without corresponding API-level enforcement — the API endpoint must also enforce the gate.
Pass criteria: Enumerate all premium features — at least 1 server-side gate per feature. Every premium feature has both a UI gate (so free users see an upgrade prompt) AND an API/server-side gate (so even direct API calls from free users are rejected with a 402 or 403). The server-side gate reads from a subscription check that is itself server-side verified. Report even on pass: "N premium features identified, all N have both UI and API gates."
Fail criteria: Premium features are gated in the UI but the underlying API routes accept requests from any authenticated user. Subscription status is checked only in client components without server-side enforcement. A premium feature is not gated at all. Do NOT pass if even 1 premium API endpoint lacks server-side enforcement.
Skip (N/A) when: No subscription tiers or premium features detected — the project has no paid plans.
Detail on fail: "API route /api/reports/export serves CSV data without checking subscription — only the UI button is hidden from free users" or "Premium AI feature at /api/ai/generate has no subscription check"
Remediation: UI-only gating is security theater — any user can call the API directly. Add server-side enforcement at the API layer:
// app/api/reports/export/route.ts
export async function GET(req: Request) {
const session = await getServerSession()
const user = await db.user.findUnique({ where: { id: session.userId } })
if (user?.plan !== 'pro' || user?.subscription_status !== 'active') {
return Response.json({ error: 'Upgrade required' }, { status: 402 })
}
// ... serve the premium content
}