Discovered during ad-hoc testing on PR #284. Full context in .claude/notes/2026-05-20-signup-flow-adhoc-findings.md — Finding 4.
GET /signup/payment?email=victim@example.com is unauthenticated and prefills the Stripe pricing-table's customer-email with whatever's in the URL. No session check, no CSRF (it's a GET), no signup-state check.
Implications
An attacker can craft a link to /signup/payment?email=victim@example.com and pay via their own card. Stripe's customer_email gets set to victim@example.com. When ST2DH receives the webhook, it looks up the DH member by that email and writes stripe_subscription_id + stripe_product_id into the victim's member row.
Net effect: an attacker can attach their Stripe subscription (and any future events tied to it) to a victim's PS1 record, with the victim having no knowledge.
Whether this leads to a further escalation depends on ST2DH's subsequent webhook handling, but the linkage primitive is there.
Suggested approach (needs design)
Three rough options — design conversation required:
- Signed token:
/signup/payment accepts a short-lived signed token (issued by /signup/submit) rather than a raw email. Token bound to the just-created member_id.
- Session-bound: drop the
?email= query param entirely; always read session['signup_email'] set by /signup/submit. Means the user has to come from the submit step (back-button still works because of session persistence).
- Stripe-side fix: have ST2DH require the Stripe customer's email to match a member who has just submitted signup (via a short-lived nonce in Stripe customer metadata) — more complex.
Option 2 is the smallest change and probably sufficient. Worth discussing.
Context: #283, PR #284
Discovered during ad-hoc testing on PR #284. Full context in
.claude/notes/2026-05-20-signup-flow-adhoc-findings.md— Finding 4.GET /signup/payment?email=victim@example.comis unauthenticated and prefills the Stripe pricing-table'scustomer-emailwith whatever's in the URL. No session check, no CSRF (it's a GET), no signup-state check.Implications
An attacker can craft a link to
/signup/payment?email=victim@example.comand pay via their own card. Stripe'scustomer_emailgets set tovictim@example.com. When ST2DH receives the webhook, it looks up the DH member by that email and writesstripe_subscription_id+stripe_product_idinto the victim's member row.Net effect: an attacker can attach their Stripe subscription (and any future events tied to it) to a victim's PS1 record, with the victim having no knowledge.
Whether this leads to a further escalation depends on ST2DH's subsequent webhook handling, but the linkage primitive is there.
Suggested approach (needs design)
Three rough options — design conversation required:
/signup/paymentaccepts a short-lived signed token (issued by/signup/submit) rather than a raw email. Token bound to the just-created member_id.?email=query param entirely; always readsession['signup_email']set by/signup/submit. Means the user has to come from the submit step (back-button still works because of session persistence).Option 2 is the smallest change and probably sufficient. Worth discussing.
Context: #283, PR #284