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.
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:However, the confirmation endpoint appears to accept a caller-provided transaction hash and only applies a local shape check before completing the purchase:
After that local check, the route decrements inventory, marks the payment complete, and returns the digital product:
The concern is the current source-to-sink chain:
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 unrelated0x...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-paymentwould 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:
payment_idas pending state only, not as sufficient authority to unlock the product.