@@ -107,7 +107,7 @@ describe('BTC:', () => {
107107 } ) ;
108108 } ) ;
109109
110- describe ( 'Unspent management spoofability (BUILD_SIGN_SEND)' , ( ) => {
110+ describe ( 'Unspent management spoofability - Consolidation (BUILD_SIGN_SEND)' , ( ) => {
111111 let coin : Tbtc ;
112112 let bitgoTest : TestBitGoAPI ;
113113 before ( ( ) => {
@@ -177,4 +177,75 @@ describe('BTC:', () => {
177177 ) ;
178178 } ) ;
179179 } ) ;
180+
181+ describe ( 'Unspent management spoofability - Fanout (BUILD_SIGN_SEND)' , ( ) => {
182+ let coin : Tbtc ;
183+ let bitgoTest : TestBitGoAPI ;
184+ before ( ( ) => {
185+ bitgoTest = TestBitGo . decorate ( BitGoAPI , { env : 'test' } ) ;
186+ bitgoTest . safeRegister ( 'tbtc' , Tbtc . createInstance ) ;
187+ bitgoTest . initializeTestVars ( ) ;
188+ coin = bitgoTest . coin ( 'tbtc' ) as Tbtc ;
189+ } ) ;
190+
191+ it ( 'should detect hex spoofing in fanout BUILD_SIGN_SEND' , async ( ) : Promise < void > => {
192+ const {
193+ getDefaultWalletKeys,
194+ toKeychainObjects,
195+ } = require ( '../../../bitgo/test/v2/unit/coins/utxo/util/keychains' ) ;
196+ const rootWalletKey = getDefaultWalletKeys ( ) ;
197+ const keysObj = toKeychainObjects ( rootWalletKey , 'pass' ) ;
198+
199+ const { Wallet } = await import ( '@bitgo/sdk-core' ) ;
200+ const wallet = new Wallet ( bitgoTest , coin , {
201+ id : '5b34252f1bf349930e34020a' ,
202+ coin : 'tbtc' ,
203+ keys : keysObj . map ( ( k ) => k . id ) ,
204+ } ) ;
205+
206+ const originalPsbt = utxolib . testutil . constructPsbt (
207+ [ { scriptType : 'p2wsh' as const , value : BigInt ( 10000 ) } ] ,
208+ [ { address : 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7' , value : BigInt ( 9000 ) } ] ,
209+ coin . network ,
210+ rootWalletKey ,
211+ 'unsigned' as const
212+ ) ;
213+ utxolib . bitgo . addXpubsToPsbt ( originalPsbt , rootWalletKey ) ;
214+
215+ const spoofedPsbt = utxolib . testutil . constructPsbt (
216+ [ { scriptType : 'p2wsh' as const , value : BigInt ( 10000 ) } ] ,
217+ [ { address : 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn' , value : BigInt ( 9000 ) } ] ,
218+ coin . network ,
219+ rootWalletKey ,
220+ 'unsigned' as const
221+ ) ;
222+ utxolib . bitgo . addXpubsToPsbt ( spoofedPsbt , rootWalletKey ) ;
223+ const spoofedHex : string = spoofedPsbt . toHex ( ) ;
224+
225+ const bgUrl : string = ( bitgoTest as any ) . _baseUrl ;
226+ const nock = require ( 'nock' ) ;
227+
228+ nock ( bgUrl )
229+ . post ( `/api/v2/${ wallet . coin ( ) } /wallet/${ wallet . id ( ) } /fanoutUnspents` )
230+ . reply ( 200 , { txHex : spoofedHex , fanoutId : 'test' } ) ;
231+
232+ nock ( bgUrl )
233+ . post ( `/api/v2/${ wallet . coin ( ) } /wallet/${ wallet . id ( ) } /tx/send` )
234+ . reply ( ( requestBody : any ) => {
235+ if ( requestBody ?. txHex === spoofedHex ) {
236+ throw new Error ( 'Spoofed transaction was sent: spoofing protection failed' ) ;
237+ }
238+ return [ 200 , { txid : 'test-txid-123' , status : 'signed' } ] ;
239+ } ) ;
240+
241+ keysObj . forEach ( ( k , i ) => nock ( bgUrl ) . get ( `/api/v2/${ wallet . coin ( ) } /key/${ wallet . keyIds ( ) [ i ] } ` ) . reply ( 200 , k ) ) ;
242+
243+ await assert . rejects (
244+ wallet . fanoutUnspents ( { walletPassphrase : 'pass' } ) ,
245+ ( e : any ) =>
246+ typeof e ?. message === 'string' &&
247+ e . message . includes ( 'prebuild attempts to spend to unintended external recipients' )
248+ ) ;
249+ } ) ;
250+ } ) ;
180251} ) ;
0 commit comments