Skip to content

Potential shallow payment confirmation issue: transaction hash shape unlocks vending purchase #2

Description

@chenshj73

Potential shallow payment confirmation issue: transaction hash shape unlocks vending purchase

Hi, I noticed a possible shallow payment-confirmation issue while reviewing the current vending-machine example.

In x402-vending-machine/server.js, the purchase endpoint records a pending payment obligation with the selected token, price, and product:

177: const paymentId = uuidv4();
179: pendingPayments.set(paymentId, {
180:   productId,
181:   productName: product.name,
182:   amount: product.price,
183:   currency: 'USD',
184:   selectedToken: selectedToken,
185:   cryptoAmount: cryptoAmount,
186:   tokenInfo: tokenInfo,
187:   timestamp: new Date(),
188:   status: 'pending'
189: });

However, the confirmation endpoint appears to accept a caller-provided transaction hash and only applies a local shape check before completing the purchase:

109: function simulateTransactionVerification(txHash, amount, token) {
110:     // In production, this would verify the transaction on the blockchain
111:     console.log(`Verifying ${token} transaction: ${txHash} for amount: ${amount}`);
112:     return txHash && txHash.length >= 32 && txHash.startsWith('0x');
113: }
230: app.post('/confirm-payment', (req, res) => {
231:     const { payment_id, transaction_hash } = req.body;
239:     const payment = pendingPayments.get(payment_id);
246:     const isVerified = simulateTransactionVerification(
247:         transaction_hash,
248:         payment.cryptoAmount,
249:         payment.selectedToken
250:     );
252:     if (!isVerified) {
253:         return res.status(400).json({ error: 'Transaction verification failed' });
254:     }

After that local check, the route decrements inventory, marks the payment complete, and returns the digital product:

267: product.stock--;
270: payment.status = 'completed';
271: payment.transactionHash = transaction_hash;
274: const digitalProduct = generateDigitalProduct(product);
277: res.json({
278:     success: true,
279:     message: 'Payment confirmed! Product delivered.',
280:     transaction_hash,
281:     product: digitalProduct,

The concern is the current source-to-sink chain:

source: request body transaction_hash
transform: simulateTransactionVerification checks only 0x prefix and length
sink: inventory is decremented and digital product is delivered

The pending payment record contains the intended cryptoAmount, token, and product, but the confirmation check does not appear to verify that the transaction actually exists on-chain, was sent by the payer, paid the expected recipient, used the expected token/network, or covered the expected amount. A fabricated or unrelated 0x... value with sufficient length may therefore satisfy the gate in this example.

If this file is only meant as a non-production demo, a clear warning around /confirm-payment would help prevent copy/paste reuse. If it is intended to model the real payment flow, the confirmation step should bind the transaction to the stored payment obligation before releasing the product.

Possible hardening directions:

  • Replace the local shape check with chain or facilitator verification.
  • Bind the transaction to recipient, asset, network, amount, payer, and payment id.
  • Treat payment_id as pending state only, not as sufficient authority to unlock the product.
  • Make the demo-only behavior explicit in code comments and README deployment guidance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions