The WUI uses a Pinia store (src/stores/app.ts) that persists certain state to
localStorage to survive hard redirects during Stripe/PayPal payment flows and
Tesla OAuth authorization. This document explains why this is the standard approach
and not a security threat.
| Key | Example Value | Purpose |
|---|---|---|
cpt-payment-id |
pi_3Abc... |
Stripe/PayPal payment intent ID |
cpt-selected-plan |
"basic" |
Selected pricing tier |
cpt-payment-method |
"stripe" |
Payment gateway used |
cpt-current-step |
"oauth" |
Wizard position for resume |
cpt-selected-brand-id |
"tesla" |
Vehicle brand selection |
cpt-selected-model-id |
"model3" |
Vehicle model selection |
| Field | Storage | Why |
|---|---|---|
stripeClientSecret |
memory only | Stripe client_secret — sensitive credential |
sessionId |
memory only | Tesla OAuth session — one-time use, 15min TTL |
vehicleReport |
memory only | User's vehicle data — PII |
vehicleData |
memory only | Vehicle specs — tied to user identity |
Reference: src/stores/app.ts lines 57-59.
A Stripe Payment Intent ID (pi_3Abc...) cannot be used to:
- Charge anyone or initiate new payments
- Access card numbers, CVVs, or billing addresses
- Modify or cancel existing payments
- Retrieve sensitive customer data
Stripe explicitly documents that Payment Intent IDs are safe to expose client-side. They are lookup references, not authorization tokens. The same applies to PayPal order IDs.
The actual sensitive credential — Stripe's client_secret — is correctly stored
in memory only (stripeClientSecret ref, line 57) and is never written to any
persistent storage.
Payment gateways (Stripe, PayPal) and OAuth providers (Tesla, Google, etc.) perform hard browser redirects that completely unload the SPA. When the user returns:
Every SPA that integrates payment gateways uses localStorage or sessionStorage
to preserve flow context across these redirects. This pattern appears in:
code_verifier)Without persistence, a user returning from Stripe/PayPal/Tesla would:
This is both a UX failure and a financial risk (duplicate charges, abandoned flows).
localStorage is bound by the Same-Origin Policy — only JavaScript running on
the exact same origin (protocol + domain + port) can read it. The only way an
attacker can access localStorage data is through a Cross-Site Scripting (XSS)
vulnerability on the application's own domain.
However, if an attacker achieves XSS, they can read all client-side storage equally:
| Storage Mechanism | Accessible via XSS? | Accessible Cross-Origin? |
|---|---|---|
localStorage |
Yes | No |
sessionStorage |
Yes | No |
| Cookies (JS-accessible) | Yes | No (with SameSite) |
| JavaScript variables | Yes | No |
The defense against this is preventing XSS, not avoiding localStorage.
The WUI mitigates XSS through:
v-html on user input)eval() or dynamic script injectionnpm audit| Threat | Risk | Mitigation |
|---|---|---|
| Attacker reads localStorage | Low | Same-Origin Policy blocks cross-origin access |
| XSS reads payment IDs | Low | Payment IDs are non-sensitive lookup references |
| XSS reads stripeClientSecret | None | Not in localStorage, memory-only |
| XSS reads Tesla session | None | Not in localStorage, memory-only |
| XSS reads vehicle report | None | Not in localStorage, memory-only |
| Stale data after tab close | Low | Data is non-sensitive; reset() clears all |
| Shared computer reads storage | Low | Only plan name and payment ID visible |
One minor hardening option: migrate from localStorage to sessionStorage for
payment-related keys. The behavior difference:
| Feature | localStorage | sessionStorage |
|---|---|---|
| Survives redirects | Yes | Yes (same tab) |
| Survives tab close | Yes | No (auto-cleared) |
| Shared across tabs | Yes (same origin) | No (per-tab isolation) |
| Security model | Same-Origin Policy | Same-Origin Policy |
sessionStorage would auto-clear stale payment state when the user closes the tab,
reducing the window of data exposure on shared computers. However, functionally both
are equivalent in terms of security since neither is accessible cross-origin.
Trade-off: sessionStorage would break the flow if the user opens the callback
in a new tab (some mobile browsers do this for OAuth redirects). localStorage is
the safer default for redirect-based flows.
Persisting payment intent IDs and UI selections to localStorage is the
industry-standard pattern for SPAs integrating payment gateways and OAuth
providers. The sensitive credentials (stripeClientSecret, sessionId,
vehicleReport) are correctly kept in transient memory. No security remediation
is required.