Trial period enforces feature limits
Why it matters
A trial period stored in a cookie, localStorage, or a client-resettable field is effectively no trial at all — users can restart it indefinitely by clearing storage or creating new accounts. CWE-284 (Improper Access Control) and CWE-602 (Client-Side Enforcement of Server-Side Security) apply. OWASP A01 (Broken Access Control) lists this under privilege escalation. The trial must be fully managed by the payment provider: Stripe tracks trial_end, emits customer.subscription.trial_will_end three days before expiry, and transitions the status automatically.
Severity rationale
High because client-manipulable trial state allows indefinite free access to paid features with no payment ever required.
Remediation
Let Stripe own the trial lifecycle. Store only the subscription.status from Stripe webhooks — when it transitions from trialing to active or past_due, your webhook handler updates the database accordingly.
// When creating a subscription with trial via Stripe:
await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
trial_period_days: 14,
})
// Stripe emits customer.subscription.trial_will_end 3 days before expiry
// and customer.subscription.updated when trial ends (status → active or past_due)
In your webhook handler, treat trialing the same as active for access decisions, and revoke access when subscription.status moves to any non-active state. Never read trial state from the client.
Detection
-
ID:
trial-enforces-limits -
Severity:
high -
What to look for: Determine whether the application has a trial period (look for
trial,trial_end,trialingin schema files, subscription status checks, or Stripe trial period configuration). If a trial exists, examine whether it enforces the same limits as the paid tier it represents, or whether it provides unlimited access during the trial. Also check for trial extension vulnerabilities: can a user create a new account to restart the trial? Is trial status stored only in the JWT? -
Pass criteria: Count every trial-related code path — at least 1 server-side expiry check must exist. Trial users have access to paid features for the trial duration, AND the trial period is enforced server-side (cannot be extended by client manipulation). When the trial ends, access is revoked the same way a subscription cancellation would be. If no trial exists, this check passes.
-
Fail criteria: Trial status is stored in a cookie or localStorage and can be manipulated. Trial period can be restarted by creating a new account with no friction. Trial end is not enforced — access continues indefinitely after trial expiry.
-
Skip (N/A) when: No trial period detected in the application. Signal: no
trial,trial_end, ortrialingreferences in schema or subscription handling code, and no Stripe trial period configuration. -
Detail on fail:
"Trial end date stored in localStorage and read client-side — can be manipulated"or"Stripe subscription shows trialing status but application grants full paid access after trial_end without re-verification" -
Remediation: Treat trial status the same way you treat paid subscription status — managed entirely by your payment provider and enforced server-side. Let Stripe manage the trial:
// When creating a subscription with trial await stripe.subscriptions.create({ customer: customerId, items: [{ price: priceId }], trial_period_days: 14, }) // Stripe sends customer.subscription.trial_will_end webhook 3 days before // and customer.subscription.updated when trial ends (status changes to active or past_due)Store
subscription.status === 'trialing'from the webhook in your database. On trial end, Stripe sends another webhook — your handler updates the database to require payment.
External references
- cwe · CWE-284 — Improper Access Control
- owasp:2021 · A01 — Broken Access Control
- cwe · CWE-602 — Client-Side Enforcement of Server-Side Security
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-billing·automated