Skip to content

MPP payment credentials and Tempo transaction hashes can be replayed in mpp-java #15

Description

@chenshj73

Hi, updating this issue with the concrete finding and expanded code references.

Finding

mpp-java verifies MPP credentials statelessly and does not claim either the challenge ID or the Tempo transaction hash before returning a successful receipt. As a result, a valid Authorization header or a push-mode transaction hash can be reused during the challenge validity window to obtain repeated protected resource delivery for a single payment.

Reviewed Code

Reviewed commit: d0cbe746133417cf1c7aa8ae26f5cca880b6cd5e.

Key Excerpts

src/main/java/com/stripe/mpp/server/Verify.java:33-58

   33 |     public static VerifyResult verifyOrChallenge(
   34 |         String authorization,
   35 |         Intent intent,
   36 |         Map<String, Object> request,
   37 |         String realm,
  ... | ...
   54 |         try {
   55 |             credential = Credential.fromAuthorization(paymentScheme);
   56 |         } catch (ParseException e) {
   57 |             return new VerifyResult.Challenged(createChallenge(methodName, intent, request, realm, secretKey, description, meta, expires));
   58 |         }

src/main/java/com/stripe/mpp/methods/tempo/TempoChargeIntent.java:88-105

   88 |     private Receipt verifyHash(String txHash, Map<String, Object> request) {
   89 |         return awaitReceipt(txHash, request);
   90 |     }
   91 | 
   92 |     private Receipt awaitReceipt(String txHash, Map<String, Object> request) {
  ... | ...
  101 |                         "transaction logs contain no Transfer matching the request currency, recipient, and amount"
  102 |                     );
  103 |                 }
  104 |                 return Receipt.success(txHash, "tempo");
  105 |             }

Why This Matters

A malicious client can pay once, keep the accepted credential or tx hash, and replay it against the same paid route while the challenge remains unexpired. Because the verifier is stateless, each replay can be accepted as a fresh successful payment and the protected resource can be delivered again without an additional transfer.

For Tempo hash mode, any previously submitted transaction hash that matches the same amount/currency/recipient/sender can be accepted repeatedly because the hash is never marked used. This violates the MPP core single-use/exactly-once payment proof requirement.

Suggested Fix

Require a durable atomic replay store in the server verifier path. Claim challenge.id before or during verification, and claim a canonical Tempo transaction hash before accepting push or pull payments. If a claim already exists, reject the credential as a replay. The store needs atomic compare-and-set semantics to handle concurrent requests.

Thanks for taking a look. If you prefer to move detailed follow-up into a GitHub Security Advisory, please invite @chenshj73; otherwise this public issue should be enough to identify and fix the bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions