@@ -106,4 +106,74 @@ describe('BTC:', () => {
106106 ) ;
107107 } ) ;
108108 } ) ;
109+
110+ describe ( 'Unspent management spoofability (BUILD_SIGN_SEND)' , ( ) => {
111+ let coin : Tbtc ;
112+ let bitgoTest : TestBitGoAPI ;
113+ before ( ( ) => {
114+ bitgoTest = TestBitGo . decorate ( BitGoAPI , { env : 'test' } ) ;
115+ bitgoTest . safeRegister ( 'tbtc' , Tbtc . createInstance ) ;
116+ bitgoTest . initializeTestVars ( ) ;
117+ coin = bitgoTest . coin ( 'tbtc' ) as Tbtc ;
118+ } ) ;
119+
120+ it ( 'should detect hex spoofing in BUILD_SIGN_SEND' , async ( ) : Promise < void > => {
121+ const { getDefaultWalletKeys, toKeychainObjects } = require ( '../../../bitgo/test/v2/unit/coins/utxo/util/keychains' ) ;
122+ const rootWalletKey = getDefaultWalletKeys ( ) ;
123+ const keysObj = toKeychainObjects ( rootWalletKey , 'pass' ) ;
124+
125+ const { Wallet } = await import ( '@bitgo/sdk-core' ) ;
126+ const wallet = new Wallet ( bitgoTest , coin , {
127+ id : '5b34252f1bf349930e34020a' ,
128+ coin : 'tbtc' ,
129+ keys : keysObj . map ( ( k ) => k . id ) ,
130+ } ) ;
131+
132+ const originalPsbt = utxolib . testutil . constructPsbt (
133+ [ { scriptType : 'p2wsh' as const , value : BigInt ( 10000 ) } ] ,
134+ [ { address : 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7' , value : BigInt ( 9000 ) } ] ,
135+ coin . network ,
136+ rootWalletKey ,
137+ 'unsigned' as const
138+ ) ;
139+ utxolib . bitgo . addXpubsToPsbt ( originalPsbt , rootWalletKey ) ;
140+
141+ const spoofedPsbt = utxolib . testutil . constructPsbt (
142+ [ { scriptType : 'p2wsh' as const , value : BigInt ( 10000 ) } ] ,
143+ [ { address : 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn' , value : BigInt ( 9000 ) } ] ,
144+ coin . network ,
145+ rootWalletKey ,
146+ 'unsigned' as const
147+ ) ;
148+ utxolib . bitgo . addXpubsToPsbt ( spoofedPsbt , rootWalletKey ) ;
149+ const spoofedHex : string = spoofedPsbt . toHex ( ) ;
150+
151+ const bgUrl : string = ( bitgoTest as any ) . _baseUrl ;
152+ const nock = require ( 'nock' ) ;
153+
154+ nock ( bgUrl )
155+ . post ( `/api/v2/${ wallet . coin ( ) } /wallet/${ wallet . id ( ) } /consolidateUnspents` )
156+ . reply ( 200 , { txHex : spoofedHex , consolidateId : 'test' } ) ;
157+
158+ nock ( bgUrl )
159+ . post ( `/api/v2/${ wallet . coin ( ) } /wallet/${ wallet . id ( ) } /tx/send` )
160+ . reply ( ( requestBody : any ) => {
161+ if ( requestBody ?. txHex === spoofedHex ) {
162+ throw new Error ( 'Spoofed transaction was sent: spoofing protection failed' ) ;
163+ }
164+ return [ 200 , { txid : 'test-txid-123' , status : 'signed' } ] ;
165+ } ) ;
166+
167+ keysObj . forEach ( ( k , i ) =>
168+ nock ( bgUrl ) . get ( `/api/v2/${ wallet . coin ( ) } /key/${ wallet . keyIds ( ) [ i ] } ` ) . reply ( 200 , k )
169+ ) ;
170+
171+ await assert . rejects (
172+ wallet . consolidateUnspents ( { walletPassphrase : 'pass' } ) ,
173+ ( e : any ) =>
174+ typeof e ?. message === 'string' &&
175+ e . message . includes ( 'prebuild attempts to spend to unintended external recipients' )
176+ ) ;
177+ } ) ;
178+ } ) ;
109179} ) ;
0 commit comments