All 22 checks with why-it-matters prose, severity, and cross-references to related audits.
Collecting raw card numbers, CVV, or expiry dates in your own form and submitting them to your server puts you directly in scope for PCI DSS compliance (pci-dss:4.0 Req 4.2.1, Req 6.4.3). That compliance burden means annual audits, penetration tests, and strict infrastructure controls — or, if you skip it, catastrophic liability when a breach occurs. OWASP A05 (Security Misconfiguration) covers exactly this failure mode: trusting that a payment provider SDK is installed is not the same as using it. Every payment flow that touches raw card data on your server is an exposure vector.
Why this severity: Critical because raw card data transiting your server creates direct PCI DSS scope and breach liability with no mitigating controls.
saas-billing.payment-security.pci-compliant-providerSee full patternStoring raw card numbers, CVV, or full PANs in your database is a direct violation of PCI DSS Req 3.3.1 and Req 3.5.1 (pci-dss:4.0), classified under OWASP A02 (Cryptographic Failures) and CWE-312 (Cleartext Storage of Sensitive Information). A single database breach exposes every stored card to attackers. Payment providers deliberately design their systems so your application never needs to hold this data — storing it anyway buys you nothing except unlimited liability.
Why this severity: Critical because any stored raw card data is immediately exploitable in a breach and constitutes a per-se PCI DSS violation regardless of encryption.
saas-billing.payment-security.no-cc-data-storedSee full patternAn unverified webhook endpoint is an open door for attackers to forge billing events — triggering subscription upgrades, skipping payments, or issuing refunds with no real money involved. CWE-345 (Insufficient Verification of Data Authenticity) is the exact failure; OWASP A08 (Software and Data Integrity Failures) and NIST SP 800-53 SI-7 (Software, Firmware, and Information Integrity) both call this out. Stripe's `constructEvent()` verifies an HMAC-SHA256 signature over the raw request body — if you parse the body to JSON first, the signature will never match, leaving you either always-failing or always-skipping the check.
Why this severity: Critical because a missing or broken signature check lets any attacker forge payment confirmation events, enabling free access to paid features without a real payment.
saas-billing.payment-security.webhook-signature-verificationSee full patternAny API route that accepts a client-supplied `plan`, `tier`, or `subscription_status` and writes it directly to the database is a privilege escalation vulnerability. OWASP A01 (Broken Access Control) and CWE-602 (Client-Side Enforcement of Server-Side Security) describe this precisely: the client is never a trusted source of truth about its own entitlements. An attacker can upgrade themselves to any plan tier with a single crafted POST request. PCI DSS Req 8.2.1 requires that access to payment-related data is based on verified identity, not client assertion.
Why this severity: Critical because any authenticated user can self-assign paid plan access with a single API call, bypassing payment entirely.
saas-billing.payment-security.no-payment-bypass-apiSee full patternJWTs and session cookies have fixed expiry windows. If a subscription lapses between token issuances, the user retains premium access until their next login — potentially days or weeks. CWE-613 (Insufficient Session Expiration) and CWE-602 (Client-Side Enforcement of Server-Side Security) both apply. NIST AC-3 (Access Enforcement) requires that access decisions happen at the enforcement point, not at authentication time. Reading subscription status from the database on each request is the only way to ensure that a cancellation or failed payment takes effect immediately.
Why this severity: High because stale JWT claims allow canceled subscribers to retain paid access for the full token lifetime without any server-side re-verification.
saas-billing.subscription-mgmt.subscription-server-verifiedSee full patternHiding 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.
Why this severity: High because UI-only gating leaves every premium API endpoint exploitable by any authenticated user with a basic HTTP client.
saas-billing.subscription-mgmt.feature-access-gatedSee full patternA 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.
Why this severity: High because client-manipulable trial state allows indefinite free access to paid features with no payment ever required.
saas-billing.subscription-mgmt.trial-enforces-limitsSee full patternIf your webhook handler doesn't process `customer.subscription.updated` events for downgrades and cancellations, plan data in your database goes stale — canceled subscribers retain premium access indefinitely. OWASP A01 (Broken Access Control) and CWE-284 (Improper Access Control) apply, plus ISO 25010 functional correctness: the system doesn't do what it claims. The `cancel_at_period_end` case is the most commonly missed: Stripe sends an `updated` event when a subscription is scheduled to cancel, not a `deleted` event — only the `deleted` event fires when cancellation actually completes.
Why this severity: High because unhandled downgrade and cancellation webhooks leave former paid users with permanent premium access after their subscription ends.
saas-billing.subscription-mgmt.downgrade-removes-accessSee full patternImmediately locking out a user on first payment failure destroys retention for what is often a recoverable situation — expired cards, temporary bank blocks, and 3DS challenges account for a large share of first-failure declines. Stripe's Smart Retries window is typically 4 attempts over 7–28 days. Without handling `invoice.payment_failed`, you either lose customers unnecessarily on first failure, or retain them indefinitely on final failure because you never processed the cancellation event. ISO 25010 fault-tolerance requires the system to degrade gracefully during partial failures.
Why this severity: Medium because the failure degrades revenue recovery and user experience significantly but does not create a security vulnerability or data loss.
saas-billing.subscription-mgmt.payment-failure-graceSee full patternUsers who cannot retrieve their own invoices open support tickets for every tax filing, expense report, and accounting reconciliation, burning engineering and support cycles on a solved problem. For B2B customers, missing invoice access blocks procurement workflows — finance teams will not pay invoices they cannot download as PDFs with proper tax metadata. This maps directly to user-experience taxons and creates churn risk when customers cannot close their books at month-end.
Why this severity: Medium because it causes sustained support load and B2B procurement friction without directly exposing data or blocking sign-ups.
saas-billing.subscription-mgmt.invoice-generationSee full patternWhen `charge.refunded` webhooks go unhandled, the application keeps granting paid features to users who have already been refunded — a direct revenue leak and a fraud vector where attackers chargeback a subscription then continue using the product indefinitely. Missing refund workflows also violate consumer-protection requirements in jurisdictions with mandatory refund windows (EU distance selling, California auto-renewal laws), exposing the business to regulatory complaints and chargeback disputes.
Why this severity: Medium because refunded users retain paid access, creating ongoing revenue leakage and chargeback-fraud exposure across the subscriber base.
saas-billing.subscription-mgmt.refund-flowSee full patternStripe retries failed webhook deliveries up to 3 days after the first failure. If your webhook handler uses plain `db.create()` or `db.insert()` rather than upserts, a retried event will either throw a unique constraint violation (silently dropping the event) or insert a duplicate row (corrupting subscription state). CWE-345 (Insufficient Verification of Data Authenticity) and ISO 25010 functional-correctness both apply. Handlers that perform slow synchronous operations (email sends, third-party API calls) before returning 200 also cause Stripe to time out and trigger additional retries, compounding the problem.
Why this severity: Medium because non-idempotent handlers corrupt subscription state on retry and can leave billing records permanently inconsistent.
saas-billing.subscription-mgmt.webhook-retry-handlingSee full patternWhen the feature list on your pricing page diverges from backend enforcement, users pay for access that doesn't exist or get features they shouldn't have. OWASP A01 (Broken Access Control) and CWE-284 (Improper Access Control) cover the enforcement gaps. Beyond security, pricing-backend mismatches erode customer trust when a paid feature is silently accessible to free users — competitors can exploit this, and paying customers feel deceived when they discover the paywall was optional.
Why this severity: High because features advertised as paid-only but unenforced in the backend allow free users to exploit premium capabilities, and features listed but not delivered constitute misrepresentation.
saas-billing.pricing-enforcement.pricing-page-matches-backendSee full patternA 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.
Why this severity: High because unenforced quantitative limits allow free users to consume unlimited resources, eroding conversion incentives and creating unfair capacity load.
saas-billing.pricing-enforcement.free-tier-limits-enforcedSee full patternUsage-based billing only works if usage is recorded server-side at the moment of the metered action. Client-side usage tracking (localStorage, session state) can be cleared, manipulated, or simply dropped on session expiration — meaning you never bill for real consumption. CWE-20 (Improper Input Validation) applies when client-supplied usage counts are trusted. ISO 25010 functional-correctness requires that what you charge matches what was consumed. Under-billing erodes revenue directly; over-billing from timing gaps creates customer disputes.
Why this severity: High because client-side or asynchronously-reported usage tracking is exploitable and creates systematic under-billing that compounds with scale.
saas-billing.pricing-enforcement.usage-billing-accurateSee full patternA cancellation button that doesn't call the Stripe API leaves the subscription active in Stripe — the user is still charged next month. Conversely, a cancellation that updates Stripe but has no webhook handler leaves your database showing `active` status after Stripe cancels — former subscribers retain premium access indefinitely. OWASP A01 (Broken Access Control) and CWE-284 (Improper Access Control) cover the access retention side. Failing to provide any self-serve cancellation path is also a compliance concern in many jurisdictions that require equal ease of cancellation as signup.
Why this severity: High because a broken cancellation flow either continues charging users after they cancel or grants indefinite premium access after cancellation — both are severe business failures.
saas-billing.pricing-enforcement.cancellation-flow-completeSee full patternNetwork errors and client retries are normal in distributed systems. Without idempotency keys, a retry after a timeout can create two Stripe Payment Intents for the same purchase — charging the customer twice. CWE-362 (Race Condition) and ISO 25010 functional-correctness both apply. Stripe's idempotency key mechanism guarantees that if you send the same key twice, you get the same response without creating a second charge — but you must opt in explicitly on every payment initiation call.
Why this severity: Medium because duplicate charges are recoverable via refund but cause customer trust damage, support burden, and potential chargeback risk.
saas-billing.financial-data.payment-idempotencySee full patternBilling events are financial records. When a subscription is created, upgraded, or canceled and nothing is logged, the only audit history is Stripe's own event log — which you can query through the Stripe Dashboard but cannot correlate with your application's user IDs, feature usage, or support tickets. SOC 2 CC7.2 (System Monitoring) and ISO 25010 operability both require that significant system events produce observable records. Without application-level billing logs, diagnosing a customer complaint about an incorrect charge or missed downgrade requires manual cross-referencing between Stripe and your database.
Why this severity: Low because missing billing logs don't create immediate security or data integrity risk, but significantly impair incident investigation and financial reconciliation.
saas-billing.financial-data.billing-audit-trailSee full patternWithout a self-service billing portal, every payment-method update, card expiration, and address change becomes a support ticket — and a meaningful percentage of involuntary churn comes from expired cards that users would have replaced themselves if given the option. Enterprise buyers specifically screen for self-service billing during procurement because their finance teams refuse to route routine card updates through a vendor's support queue.
Why this severity: Low because the impact is involuntary-churn drag and support-ticket volume rather than data exposure or outage.
saas-billing.financial-data.customer-portalSee full patternFloating-point arithmetic on monetary amounts produces silent rounding errors — `0.1 + 0.2` is `0.30000000000000004` in JavaScript. When tax, discounts, or proration are calculated using float dollars instead of integer cents, the errors compound across transactions. CWE-681 (Incorrect Conversion Between Numeric Types) covers this, and ISO 25010 functional-correctness requires that calculations produce accurate results. Formatting currency with string concatenation (`"$" + amount`) produces locale-incorrect output (wrong decimal separators, missing grouping) and breaks for non-USD currencies.
Why this severity: Low because floating-point rounding errors in typical SaaS pricing are small per transaction, but compound at scale and can produce incorrect invoice totals.
saas-billing.financial-data.currency-handlingSee full patternSales tax and VAT obligations for SaaS products vary by jurisdiction and are expanding: the EU VAT Directive (2006/112/EC) applies to digital services sold to EU consumers regardless of where the seller is based, and US economic nexus thresholds mean many SaaS businesses now have multi-state obligations. Failing to collect and remit applicable taxes creates retroactive liability — tax authorities can assess back taxes plus penalties. This is an informational check because the legal picture varies significantly, but ignoring the question entirely is a clear risk signal for any product with meaningful revenue.
Why this severity: Info because tax obligations are jurisdiction-dependent and the check flags unanswered questions rather than definitive violations — but the risk is real for products with non-trivial revenue.
saas-billing.financial-data.tax-calculationSee full patternA subscription product with no billing page in account settings signals to users that the product is unfinished or abandoned — they cannot see their current plan, next charge date, or payment method, so they have no way to verify they are being billed correctly. This drives trust-based churn and triggers chargebacks from users who forgot they were subscribed because the product never surfaced their billing state in context.
Why this severity: Info because the page itself is a baseline usability expectation rather than a defect with measurable security or revenue impact.
saas-billing.financial-data.billing-page-accessibleSee full patternRun this audit in your AI coding tool (Claude Code, Cursor, Bolt, etc.) and submit results here for scoring and benchmarks.
Open Billing & Payments Audit