A pricing page that advertises a 3-project limit is a business promise — if the API enforces no such limit, every free user can exploit the gap indefinitely. OWASP A01 (Broken Access Control) and CWE-284 (Improper Access Control) apply directly. This is also a revenue integrity issue: without enforcement, free tier users have no incentive to upgrade, which directly undermines the conversion funnel. Display-based limits (showing only the last 5 records in the UI while the API returns all) fail this check — the limit must exist at the query or API layer.
High because unenforced quantitative limits allow free users to consume unlimited resources, eroding conversion incentives and creating unfair capacity load.
Add usage count checks to every resource creation endpoint before allowing the write. Read the limit from your central plan configuration so pricing page and API always agree.
// app/api/projects/route.ts
export async function POST(req: Request) {
const session = await getServerSession()
const user = await db.user.findUnique({
where: { id: session.userId },
include: { _count: { select: { projects: true } } }
})
const limit = PLANS[user.plan ?? 'free'].limits.projects // e.g. 3 for free, Infinity for pro
if (user._count.projects >= limit) {
return Response.json(
{ error: `Project limit reached (${limit}). Upgrade to create more.` },
{ status: 402 }
)
}
// proceed with creation
}
For display-based limits ("show last N submissions"), enforce the limit in the SQL query with LIMIT N — not in the UI render layer.
ID: saas-billing.pricing-enforcement.free-tier-limits-enforced
Severity: high
What to look for: Identify the free tier limits (project count, record count, API call limits, seat limits, etc.) from the pricing page or plan configuration. Then look for server-side enforcement of those limits. Check API routes that create resources (projects, records, team members, etc.) for checks against the user's current usage count before allowing creation. Look for database queries that count existing resources and compare against plan limits before allowing new resource creation.
Pass criteria: Count every quantitative limit advertised for the free tier — at least 1 must be server-enforced. Every quantitative limit listed (project count, seat count, record limits, etc.) is enforced at the API layer before allowing new resource creation. The limit values match what the pricing page advertises. Report even on pass: "N quantitative limits found, all N enforced at API layer." Display-based and access-based quantitative limits (e.g., "show last N results," "access last N days of history") are real feature gates that must be server-enforced. A UI that advertises a limit (e.g., "last 5 submissions") while the API returns unlimited data is a FAIL — the limit must be enforced in the query or API layer, not just in the UI rendering.
Fail criteria: Free tier limits exist in the pricing page but are not enforced in API routes — free users can create unlimited resources. Resource creation endpoints do not check current usage before allowing creation.
Skip (N/A) when: Skip when no quantitative limits of any kind (creation, access, display, or usage) differentiate free and paid tiers. Signal: pricing page shows no free plan, no free tier in plan configuration, no trial-without-payment-method flow.
Detail on fail: "Pricing page shows 3-project limit for free tier but POST /api/projects has no count check — free users can create unlimited projects" or "Free tier seat limit is documented but POST /api/team/invite has no team member count enforcement"
Remediation: Add usage checks to resource creation endpoints:
// app/api/projects/route.ts
export async function POST(req: Request) {
const session = await getServerSession()
const user = await db.user.findUnique({
where: { id: session.userId },
include: { _count: { select: { projects: true } } }
})
const limit = PLANS[user.plan].limits.projects
if (user._count.projects >= limit) {
return Response.json(
{ error: 'Project limit reached. Upgrade to create more projects.' },
{ status: 402 }
)
}
// ... create the project
}