Skip to content

fix(stripe): bind idempotency key to challenge id, not SPT#155

Open
MattewGraham wants to merge 2 commits into
tempoxyz:mainfrom
MattewGraham:fix/stripe-idempotency-replay
Open

fix(stripe): bind idempotency key to challenge id, not SPT#155
MattewGraham wants to merge 2 commits into
tempoxyz:mainfrom
MattewGraham:fix/stripe-idempotency-replay

Conversation

@MattewGraham

Copy link
Copy Markdown

Security advisory: duplicate Stripe charges via SPT in idempotency key

Severity: High · Class: Financial correctness / idempotency bypass · Affected: mpp.methods.stripe

Impact

A buyer can be charged twice for a single payment. Any retry of a 402 flow that
re-mints the Stripe Shared Payment Token (SPT) — the normal case, since SPTs are
short-lived and single-use — produces a different idempotency key, so Stripe
happily creates a second PaymentIntent for the same logical charge.

Where it comes from

Both PaymentIntent creation paths derived the idempotency key from the SPT:

options = {"idempotency_key": f"mppx_{challenge_id}_{spt}"}   # SDK path
"Idempotency-Key": f"mppx_{challenge_id}_{spt}"               # raw-HTTP path

Idempotency is supposed to collapse retries of the same charge into one
settlement. The challenge_id already is that identity: it is an HMAC bound to
amount, currency, recipient, realm and intent. The SPT carries no additional
identity — it is an ephemeral credential — so mixing it in only serves to make
otherwise-identical retries look distinct to Stripe.

Resolution

Key idempotency on challenge_id alone:

options = {"idempotency_key": f"mppx_{challenge_id}"}

Now a re-tried charge with a freshly minted SPT resolves to the same Stripe
PaymentIntent, and the double-charge window is closed. Both the SDK-client and
raw-HTTP code paths were updated identically so behaviour no longer depends on
which transport is configured.

Verification

tests/test_stripe.py carried two assertions that encoded the vulnerable
behaviour
(they asserted the SPT was present in the key). Those assertions were
corrected to assert challenge-only keys, and a docstring now records why the SPT
must stay out. Full test_stripe.py suite: 50 passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant