Skip to content

Commit ff6e43d

Browse files
committed
Sign MoonPay widget URLs with IP binding
MoonPay's on-ramp security upgrade refuses to load widget URLs that are not signed and bound to a hash of the customer's device IP. Route every MoonPay buy/sell widget URL through the info server's /v1/moonpay/signUrl endpoint (which captures the public IP and applies the allowedIpAddress + signature params) before opening it, in both the new ramps plugin and the still-registered legacy fiat provider.
1 parent 90034ca commit ff6e43d

5 files changed

Lines changed: 40 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased (develop)
44

5+
- changed: Sign MoonPay buy/sell widget URLs and bind them to the customer's IP via the info server, for MoonPay's on-ramp IP-matching security upgrade.
6+
57
## 4.49.0 (staging)
68

79
- added: Monero wallet import support

eslint.config.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,6 @@ export default [
455455

456456
'src/plugins/gui/providers/bityProvider.ts',
457457

458-
'src/plugins/gui/providers/moonpayProvider.ts',
459458
'src/plugins/gui/providers/mtpelerinProvider.ts',
460459

461460
'src/plugins/gui/providers/revolutProvider.ts',

src/plugins/gui/providers/moonpayProvider.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
RETURN_URL_PAYMENT,
5252
validateExactRegion
5353
} from './common'
54+
import { signMoonpayUrl } from './moonpaySign'
5455
const providerId = 'moonpay'
5556
const storeId = 'com.moonpay'
5657
const partnerIcon = 'moonpay_symbol_prp.png'
@@ -643,6 +644,7 @@ export const moonpayProvider: FiatProviderFactory = {
643644
}
644645
urlObj.set('query', queryObj)
645646
console.log('Approving moonpay buy quote url=' + urlObj.href)
647+
const signedUrl = await signMoonpayUrl(urlObj.href)
646648
const handleBuyDeeplinkAsync = async (
647649
link: unknown
648650
): Promise<void> => {
@@ -695,7 +697,7 @@ export const moonpayProvider: FiatProviderFactory = {
695697
handleBuyDeeplinkAsync(link).catch(() => {})
696698
}
697699
await showUi.openExternalWebView({
698-
url: urlObj.href,
700+
url: signedUrl,
699701
providerId,
700702
deeplinkHandler: handleBuyDeeplink
701703
})
@@ -718,6 +720,7 @@ export const moonpayProvider: FiatProviderFactory = {
718720
}
719721
urlObj.set('query', queryObj)
720722
console.log('Approving moonpay sell quote url=' + urlObj.href)
723+
const signedUrl = await signMoonpayUrl(urlObj.href)
721724

722725
let inPayment = false
723726

@@ -867,7 +870,7 @@ export const moonpayProvider: FiatProviderFactory = {
867870
onUrlChangeAsync(uri).catch(() => {})
868871
}
869872
await showUi.openWebView({
870-
url: urlObj.href,
873+
url: signedUrl,
871874
onUrlChange,
872875
onClose: () => {}
873876
})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { asObject, asString } from 'cleaners'
2+
3+
import { fetchInfo } from '../../../util/network'
4+
5+
const asMoonpaySignResponse = asObject({ signedUrl: asString })
6+
7+
/**
8+
* Ask the info server to bind a Moonpay widget URL to the caller's public IP
9+
* and sign it. Moonpay's on-ramp security upgrade refuses to load widget URLs
10+
* that are not signed and IP-bound, so every buy/sell widget URL must be routed
11+
* through here before it is opened.
12+
*/
13+
export const signMoonpayUrl = async (url: string): Promise<string> => {
14+
const response = await fetchInfo(
15+
'v1/moonpay/signUrl',
16+
{
17+
method: 'POST',
18+
headers: { 'Content-Type': 'application/json' },
19+
body: JSON.stringify({ url })
20+
},
21+
10000
22+
)
23+
if (!response.ok) {
24+
throw new Error(`Moonpay URL signing failed: ${response.status}`)
25+
}
26+
const reply = await response.json()
27+
return asMoonpaySignResponse(reply).signedUrl
28+
}

src/plugins/ramps/moonpay/moonpayRampPlugin.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
RETURN_URL_PAYMENT,
4545
validateExactRegion
4646
} from '../../gui/providers/common'
47+
import { signMoonpayUrl } from '../../gui/providers/moonpaySign'
4748
import { addTokenToArray } from '../../gui/util/providerUtils'
4849
import { rampDeeplinkManager } from '../rampDeeplinkHandler'
4950
import type {
@@ -833,9 +834,10 @@ export const moonpayRampPlugin: RampPluginFactory = (
833834
}
834835
urlObj.set('query', queryObj)
835836
console.log('Approving moonpay buy quote url=' + urlObj.href)
837+
const signedUrl = await signMoonpayUrl(urlObj.href)
836838

837839
deeplinkToken = await openExternalWebView({
838-
url: urlObj.href,
840+
url: signedUrl,
839841
deeplink: {
840842
direction: 'buy',
841843
providerId: pluginId,
@@ -993,13 +995,14 @@ export const moonpayRampPlugin: RampPluginFactory = (
993995
}
994996
urlObj.set('query', queryObj)
995997
console.log('Approving moonpay sell quote url=' + urlObj.href)
998+
const signedUrl = await signMoonpayUrl(urlObj.href)
996999

9971000
let inPayment = false
9981001

9991002
const openWebView = async (): Promise<void> => {
10001003
await new Promise<void>((resolve, reject) => {
10011004
navigation.navigate('guiPluginWebView', {
1002-
url: urlObj.href,
1005+
url: signedUrl,
10031006
onClose: () => {
10041007
resolve()
10051008
},

0 commit comments

Comments
 (0)