From 447e9604ed0a2d94b78153739d5f7be35b10e9b6 Mon Sep 17 00:00:00 2001 From: Support Bot Date: Wed, 24 Jun 2026 19:28:53 +0000 Subject: [PATCH] fix(sdk-coin-xrp): enforce destination tag in verifyTransaction The verifyTransaction comparator was only checking the base address (stripping ?dt=N), allowing transactions with a different destination tag to pass verification silently. A payment built for rXXXX?dt=123 would verify successfully even if the prebuild targeted rXXXX?dt=0, causing the transaction to land at the wrong account subdivision and never appear for the intended recipient. Compare r1Details.destinationTag against r2Details.destinationTag so that a mismatch between the intended and actual destination tag is caught before the transaction is signed and submitted. Ticket: CSHLD-1115 Co-Authored-By: Claude Session-Id: 1cceafce-d4d3-4c86-a796-8e300680dc18 Task-Id: fa37dd24-b008-4d0e-b331-5db8f5e1371d --- modules/sdk-coin-xrp/src/xrp.ts | 7 ++- modules/sdk-coin-xrp/test/unit/xrp.ts | 72 +++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/modules/sdk-coin-xrp/src/xrp.ts b/modules/sdk-coin-xrp/src/xrp.ts index 31818992b2..1834ebd987 100644 --- a/modules/sdk-coin-xrp/src/xrp.ts +++ b/modules/sdk-coin-xrp/src/xrp.ts @@ -459,7 +459,12 @@ export class Xrp extends BaseCoin { const expectedOutput = txParams.recipients && txParams.recipients[0]; const comparator = (recipient1, recipient2) => { - if (utils.getAddressDetails(recipient1.address).address !== utils.getAddressDetails(recipient2.address).address) { + const r1Details = utils.getAddressDetails(recipient1.address); + const r2Details = utils.getAddressDetails(recipient2.address); + if (r1Details.address !== r2Details.address) { + return false; + } + if (r1Details.destinationTag !== r2Details.destinationTag) { return false; } const amount1 = new BigNumber(recipient1.amount); diff --git a/modules/sdk-coin-xrp/test/unit/xrp.ts b/modules/sdk-coin-xrp/test/unit/xrp.ts index 8a8807293c..64a2ae0d63 100644 --- a/modules/sdk-coin-xrp/test/unit/xrp.ts +++ b/modules/sdk-coin-xrp/test/unit/xrp.ts @@ -920,6 +920,78 @@ describe('XRP:', function () { }); }); + describe('verifyTransaction destination tag enforcement', () => { + // txHex for: Payment from rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG to rfjub8A4dpSD5nnszUFTsLprxu1W398jwc + // DestinationTag: 0, Amount: 253481 drops + const txHexWithDt0 = + '{"TransactionType":"Payment","Account":"rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG","Destination":"rfjub8A4dpSD5nnszUFTsLprxu1W398jwc","DestinationTag":0,"Amount":"253481","Flags":2147483648,"LastLedgerSequence":1626225,"Fee":"45","Sequence":7}'; + // txHex for: same but DestinationTag: 42 + const txHexWithDt42 = + '{"TransactionType":"Payment","Account":"rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG","Destination":"rfjub8A4dpSD5nnszUFTsLprxu1W398jwc","DestinationTag":42,"Amount":"253481","Flags":2147483648,"LastLedgerSequence":1626225,"Fee":"45","Sequence":7}'; + // txHex for: same but no DestinationTag + const txHexNoDt = + '{"TransactionType":"Payment","Account":"rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG","Destination":"rfjub8A4dpSD5nnszUFTsLprxu1W398jwc","Amount":"253481","Flags":2147483648,"LastLedgerSequence":1626225,"Fee":"45","Sequence":7}'; + + it('should pass when transaction and recipient both have matching destination tag', async function () { + const result = await basecoin.verifyTransaction({ + txParams: { + recipients: [{ address: 'rfjub8A4dpSD5nnszUFTsLprxu1W398jwc?dt=0', amount: '253481' }], + }, + txPrebuild: { txHex: txHexWithDt0 }, + }); + result.should.equal(true); + }); + + it('should pass when transaction and recipient both have no destination tag', async function () { + const result = await basecoin.verifyTransaction({ + txParams: { + recipients: [{ address: 'rfjub8A4dpSD5nnszUFTsLprxu1W398jwc', amount: '253481' }], + }, + txPrebuild: { txHex: txHexNoDt }, + }); + result.should.equal(true); + }); + + it('should reject when transaction has destination tag 0 but recipient has no destination tag', async function () { + await assert.rejects( + async () => + basecoin.verifyTransaction({ + txParams: { + recipients: [{ address: 'rfjub8A4dpSD5nnszUFTsLprxu1W398jwc', amount: '253481' }], + }, + txPrebuild: { txHex: txHexWithDt0 }, + }), + { message: 'transaction prebuild does not match expected output' } + ); + }); + + it('should reject when transaction has no destination tag but recipient has destination tag 0', async function () { + await assert.rejects( + async () => + basecoin.verifyTransaction({ + txParams: { + recipients: [{ address: 'rfjub8A4dpSD5nnszUFTsLprxu1W398jwc?dt=0', amount: '253481' }], + }, + txPrebuild: { txHex: txHexNoDt }, + }), + { message: 'transaction prebuild does not match expected output' } + ); + }); + + it('should reject when transaction has destination tag 42 but recipient has destination tag 0', async function () { + await assert.rejects( + async () => + basecoin.verifyTransaction({ + txParams: { + recipients: [{ address: 'rfjub8A4dpSD5nnszUFTsLprxu1W398jwc?dt=0', amount: '253481' }], + }, + txPrebuild: { txHex: txHexWithDt42 }, + }), + { message: 'transaction prebuild does not match expected output' } + ); + }); + }); + describe('blind signing token enablement protection', () => { it('should verify as valid the enabletoken intent when prebuild tx matchs user intent ', async function () { const { txParams, txPrebuildRaw, walletData } = testData.enableTokenFixtures;