Skip to content

requireQuota fails open: an untyped quota key silently grants unlimited quota #769

Description

@InfinityBowman

requireQuota takes an untyped quota key, so a mistyped key silently grants unlimited quota.

The defect

packages/web/src/server/guards/requireQuota.ts:18 declares the parameter as quotaKey: string, even though QuotaKey = 'projects.max' | 'collaborators.org.max' already exists in packages/shared/src/plans/types.ts:20. Quotas also carries an index signature [key: string]: number (plans/types.ts:36).

const limit = (orgBilling.quotas as unknown as Record<string, number>)[quotaKey];
if (!isUnlimitedQuota(limit) && used + requested > limit) { /* deny */ }

With a key that is not in quotas:

  1. limit is undefined at runtime but typed number, because the index signature plus the absence of noUncheckedIndexedAccess hides it.
  2. isUnlimitedQuota(undefined) is undefined === -1, so false.
  3. used + requested > undefined is a NaN comparison, so false.
  4. The if does not fire and the guard returns { ok: true }.

The quota is not enforced. It fails open.

Scope

Latent, not active: both call sites in org-projects.server.ts:63 and :321 pass correct literals today. requireEntitlement.ts:48 has the same shape but fails closed, so it is lower priority.

The as unknown as Record<string, number> on line 48 is noise -- the index signature already permits that access. Removing the index signature is what does the work.

Done when

  • requireQuota takes quotaKey: QuotaKey
  • Quotas has no [key: string]: number index signature
  • The cast on line 48 is gone
  • requireEntitlement gets the same treatment for consistency (entitlement: EntitlementKey, no index signature on Entitlements, cast on line 48 gone)
  • A type test (see Carry-overs from the April 2026 TypeScript audit #777) asserting requireQuota rejects a string that is not a QuotaKey, so the index signature cannot quietly come back

Effort: ~30 minutes. No runtime behavior change for correct keys.


Part of #778 (TypeScript correctness target state and tracker).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions