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.
High because client-manipulable trial state allows indefinite free access to paid features with no payment ever required.
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.
ID: saas-billing.subscription-mgmt.trial-enforces-limits
Severity: high
What to look for: Determine whether the application has a trial period (look for trial, trial_end, trialing in 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, or trialing references 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.