feat: add allowedIp to moonpay signature generation (#3375)
What changed, and why it matters
This commit updates how Cake Wallet builds payment links for MoonPay, a service that lets users buy or sell cryptocurrency inside the app. The app now asks its own backend server to produce the full signed payment URL, instead of adding the cryptographic signature itself on the device. The backend can now also restrict the link to the user's IP address. The change is a feature/refactor; there is no direct evidence in the commit that it fixes an active security vulnerability.
Treat as a routine feature/hardening update. Review the backend /api/moonpay implementation to confirm allowedIp is validated and that the returned signed URL cannot be tampered with or replayed. Ensure the new moonPaySandboxApiKey secret is not committed in plaintext and is handled with the same controls as the production key.
Security signals we found
Backend now generates the full signed MoonPay URL rather than only a signature
New sandbox API key secret and useSandbox query parameter for test mode
Backend signature generation reportedly includes allowedIp binding
Removes client-side reassembly of signed MoonPay URI
Evidence from the diff
The patch refactors MoonPay URL generation in lib/buy/moonpay/moonpay_provider.dart. Previously the app appended an apiKey to query parameters, sent the query to a Cake backend endpoint /api/moonpay to obtain only a signature, then re-injected that signature into the local URI. Now the backend returns the complete signed query string (renamed getMoonpaySignedQuery), and the app simply parses and returns it. A sandbox API key path is added (moonPaySandboxApiKey) and the backend call gains a useSandbox flag for test environments. The commit message says the backend change adds allowedIp to signature generation, implying the signed URL can be bound to the requesting IP. No diff evidence shows a vulnerability in the old flow; it is a hardening/feature change.
Changed components
lib/buy/moonpay/moonpay_provider.darttool/utils/secret_key.dartMoonPay buy/sell integrationCake Wallet backend /api/moonpay endpoint (not in diff)Inspect captured patch +11 / −13
diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart
index dfd09e13..9b9d5dd9 100644
--- a/lib/buy/moonpay/moonpay_provider.dart
+++ b/lib/buy/moonpay/moonpay_provider.dart
@@ -79,7 +79,7 @@ class MoonPayProvider extends BuyProvider {
@override
bool get isAggregator => false;
- static String get _apiKey => secrets.moonPayApiKey;
+ String get _apiKey => isTestEnvironment ? secrets.moonPaySandboxApiKey : secrets.moonPayApiKey;
String get currencyCode => walletTypeToCryptoCurrency(wallet.type, chainId: wallet.chainId).title.toLowerCase();
@@ -96,8 +96,9 @@ class MoonPayProvider extends BuyProvider {
}
}
- Future<String> getMoonpaySignature(String query) async {
- final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
+ Future<String> getMoonpaySignedQuery(String query) async {
+ final uri =
+ Uri.https(_cIdBaseUrl, "/api/moonpay", isTestEnvironment ? {"useSandbox": "true"} : null);
final response = await ProxyWrapper().post(
clearnetUri: uri,
@@ -107,7 +108,8 @@ class MoonPayProvider extends BuyProvider {
if (response.statusCode == 200) {
- return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
+ printV((jsonDecode(response.body) as Map<String, dynamic>));
+ return (jsonDecode(response.body) as Map<String, dynamic>)['query'] as String;
} else {
throw Exception(
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
@@ -297,18 +299,13 @@ class MoonPayProvider extends BuyProvider {
required Map<String, String> params,
String? amount,
}) async {
- if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey;
+ if (_apiKey.isNotEmpty) params["apiKey"] = _apiKey;
final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
- final originalUri = Uri.https(baseUrl, '', params);
+ final originalUri = Uri.https(baseUrl, "", params);
- if (isTestEnvironment) return originalUri;
-
- final signature = await getMoonpaySignature('?${originalUri.query}');
- final query = Map<String, dynamic>.from(originalUri.queryParameters);
- query['signature'] = signature;
- final signedUri = originalUri.replace(queryParameters: query);
- return signedUri;
+ final query = await getMoonpaySignedQuery("?${originalUri.query}");
+ return Uri.parse(query);
}
Future<Order> findOrderById(String id) async {
diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart
index ebde2f07..d4024cc5 100644
--- a/tool/utils/secret_key.dart
+++ b/tool/utils/secret_key.dart
@@ -18,6 +18,7 @@ class SecretKey {
SecretKey('wyreApiKey', () => ''),
SecretKey('wyreAccountId', () => ''),
SecretKey('moonPayApiKey', () => ''),
+ SecretKey('moonPaySandboxApiKey', () => ''),
SecretKey('moonPaySecretKey', () => ''),
SecretKey('sideShiftAffiliateId', () => ''),
SecretKey('simpleSwapApiKey', () => ''),
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.