omit a paynym contact whose payment code does not parse rather than keeping it with a null code or failing the whole paynym response
What changed, and why it matters
This commit fixes a bug in Sparrow Wallet's PayNym (BIP47 reusable payment code) contact handling. Previously, if a single contact in your PayNym following/followers list had a malformed payment code, the app either kept a broken contact with a null payment code or could fail the entire PayNym response. Now it simply skips that bad contact and keeps the rest. This is a robustness fix that prevents crashes or broken contact lists, but it is not a direct theft-of-funds vulnerability.
Review the drongo submodule diff (9a8c659b49b6cb3010855b74a37e66e076521490) to confirm the PaymentCode parsing exception behavior and ensure no other callers rely on the old null-payment-code behavior. Consider whether silently omitting contacts could hide attacker-controlled deletions; logging remains present.
Security signals we found
Null payment code previously stored in contact object
Potential NullPointerException or downstream dereference of null PaymentCode in contact lists/search
Whole PayNym response could fail on one malformed contact
Defense-in-depth input validation for BIP47 payment codes
Unit test added for malformed/corrupted payment codes
Evidence from the diff
PayNym.fromString() now returns Optional
Changed components
Sparrow Wallet PayNym contact parsingPayNymService following/followers list constructiondrongo submodule PaymentCode parsingInspect captured patch +43 / −11
### drongo
@@ -1 +1 @@
-Subproject commit eb3cf3e577265165875b70de60d5333f827e279e
+Subproject commit 9a8c659b49b6cb3010855b74a37e66e076521490
### src/main/java/com/sparrowwallet/sparrow/paynym/PayNym.java
@@ -9,6 +9,7 @@
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import static com.sparrowwallet.drongo.bip47.PaymentCode.SEGWIT_SCRIPT_TYPES;
import static com.sparrowwallet.drongo.bip47.PaymentCode.V1_SCRIPT_TYPES;
@@ -70,16 +71,13 @@ public static List<ScriptType> getV1ScriptTypes() {
return V1_SCRIPT_TYPES;
}
- public static PayNym fromString(String strPaymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
- PaymentCode paymentCode;
+ public static Optional<PayNym> fromString(String strPaymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
try {
- paymentCode = new PaymentCode(strPaymentCode);
+ return Optional.of(new PayNym(new PaymentCode(strPaymentCode), nymId, nymName, segwit, following, followers));
} catch(InvalidPaymentCodeException e) {
log.error("Error creating PayNym from payment code " + strPaymentCode, e);
- paymentCode = null;
+ return Optional.empty();
}
-
- return new PayNym(paymentCode, nymId, nymName, segwit, following, followers);
}
public static PayNym fromWallet(Wallet bip47Wallet) {
### src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java
@@ -206,13 +206,14 @@ public static Observable<PayNym> getPayNym(String nymIdentifier, boolean compact
}
List<Map<String, Object>> followingMaps = (List<Map<String, Object>>)nymMap.get("following");
- List<PayNym> following = followingMaps.stream().map(followingMap -> {
- return PayNym.fromString((String)followingMap.get("code"), (String)followingMap.get("nymId"), (String)followingMap.get("nymName"), (Boolean)followingMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
+ //An entry whose payment code does not parse is omitted rather than failing the whole PayNym
+ List<PayNym> following = followingMaps.stream().flatMap(followingMap -> {
+ return PayNym.fromString((String)followingMap.get("code"), (String)followingMap.get("nymId"), (String)followingMap.get("nymName"), (Boolean)followingMap.get("segwit"), Collections.emptyList(), Collections.emptyList()).stream();
}).collect(Collectors.toList());
List<Map<String, Object>> followersMaps = (List<Map<String, Object>>)nymMap.get("followers");
- List<PayNym> followers = followersMaps.stream().map(followerMap -> {
- return PayNym.fromString((String)followerMap.get("code"), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
+ List<PayNym> followers = followersMaps.stream().flatMap(followerMap -> {
+ return PayNym.fromString((String)followerMap.get("code"), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit"), Collections.emptyList(), Collections.emptyList()).stream();
}).collect(Collectors.toList());
return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit"), following, followers);
### src/test/java/com/sparrowwallet/sparrow/paynym/PayNymTest.java
@@ -0,0 +1,33 @@
+package com.sparrowwallet.sparrow.paynym;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A contact entry in a PayNym response whose payment code does not parse is omitted, rather than becoming a PayNym with no payment code that the contact
+ * lists and search then dereference.
+ */
+public class PayNymTest {
+ private static final String PAYMENT_CODE = "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA";
+
+ @Test
+ public void parsesAValidPaymentCode() {
+ Optional<PayNym> payNym = PayNym.fromString(PAYMENT_CODE, "id", "+name", true, Collections.emptyList(), Collections.emptyList());
+ assertTrue(payNym.isPresent());
+ assertEquals(PAYMENT_CODE, payNym.get().paymentCode().toString());
+ }
+
+ @Test
+ public void omitsAnUnparseablePaymentCode() {
+ assertTrue(PayNym.fromString("not-a-payment-code", "id", "+name", true, Collections.emptyList(), Collections.emptyList()).isEmpty());
+
+ //Valid Base58 with a corrupted checksum
+ String corrupted = PAYMENT_CODE.substring(0, PAYMENT_CODE.length() - 1) + (PAYMENT_CODE.endsWith("A") ? "B" : "A");
+ assertTrue(PayNym.fromString(corrupted, "id", "+name", true, Collections.emptyList(), Collections.emptyList()).isEmpty());
+ }
+}Why this scored 37/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.