check a paynym retrieved by payment code matches the requested code
What changed, and why it matters
This commit adds a safety check in Sparrow Wallet's PayNym feature. PayNyms are human-readable names linked to Bitcoin payment codes. Previously, when a user looked up a PayNym by its payment code, the wallet would trust whatever payment code the PayNym server returned. The fix verifies that the returned payment code actually matches the one requested, preventing a malicious or compromised server from silently substituting a different payment code. The UI now also displays the wallet's own payment code rather than the server-returned one.
Users should upgrade to a Sparrow Wallet release containing this commit. Developers should review whether other server-returned fields (nymName, nymID, following lists) are also validated against the requested identifier, and consider adding tests for payment code mismatch scenarios.
Security signals we found
Server response validation added: requested vs returned payment code notification address comparison
UI now uses locally derived payment code instead of server-returned payment code
Throws IllegalStateException on mismatch, indicating a security-relevant invariant
Trust boundary between wallet client and PayNym server is tightened
Evidence from the diff
The patch modifies PayNymController and PayNymService. In PayNymService.getPayNym(), after parsing the server response, it now extracts the originally requested payment code via getRequestedPaymentCode(nymIdentifier) and compares notification addresses. If they differ, it throws IllegalStateException. In PayNymController.refresh(), paymentCode and payNymAvatar are now set from getMasterWallet().getPaymentCode() instead of payNym.paymentCode(). This closes a trust boundary issue where a PayNym server could return a payment code different from the one queried, potentially causing the wallet to display or use an attacker’s code.
Changed components
src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.javasrc/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.javaPayNym lookup/refresh functionalityPayment code display and avatar renderingInspect captured patch +16 / −2
### src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java
@@ -184,8 +184,8 @@ private void refresh() {
walletPayNym = payNym;
searchPayNyms.setDisable(false);
payNymName.setText(payNym.nymName());
- paymentCode.setPaymentCode(payNym.paymentCode());
- payNymAvatar.setPaymentCode(payNym.paymentCode());
+ paymentCode.setPaymentCode(getMasterWallet().getPaymentCode());
+ payNymAvatar.setPaymentCode(getMasterWallet().getPaymentCode());
followingList.setUserData(null);
followingList.setPlaceholder(new Label("No contacts"));
updateFollowing();
### src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java
@@ -201,6 +201,12 @@ public static Observable<PayNym> getPayNym(String nymIdentifier, boolean compact
List<Map<String, Object>> codes = (List<Map<String, Object>>)nymMap.get("codes");
PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code")));
+ //A payment code identifies itself, so a PayNym looked up by payment code must share its key and chain code, differing at most in the segwit feature bit
+ PaymentCode requestedCode = getRequestedPaymentCode(nymIdentifier);
+ if(requestedCode != null && !requestedCode.getNotificationAddress().equals(code.getNotificationAddress())) {
+ throw new IllegalStateException("PayNym server returned payment code " + code + " for requested payment code " + requestedCode);
+ }
+
if(compact) {
return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
}
@@ -220,6 +226,14 @@ public static Observable<PayNym> getPayNym(String nymIdentifier, boolean compact
});
}
+ private static PaymentCode getRequestedPaymentCode(String nymIdentifier) {
+ try {
+ return new PaymentCode(nymIdentifier);
+ } catch(InvalidPaymentCodeException e) {
+ return null;
+ }
+ }
+
public static Observable<String> getAuthToken(Wallet wallet, Map<String, Object> map) {
if(map.containsKey("token")) {
return Observable.just((String)map.get("token"));Why this scored 59/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.