Skip to content

Commit 418db6f

Browse files
tcheericclaude
andcommitted
fix(nut11): getNSigsRefund defaults to 1 on empty-values tag (review #238)
Copilot review: getNSigsRefund() called values.get(0) without an emptiness check. The deserializers accept a key-only tag array (size 1), which yields an empty values list and would throw IndexOutOfBoundsException — a DoS vector via a crafted secret. Now returns the default 1 when the tag is present but carries no value, matching the tag-absent path. Regression test added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 947a2ca commit 418db6f

2 files changed

Lines changed: 26 additions & 5 deletions

File tree

cashu-lib-common/src/main/java/xyz/tcheeric/cashu/common/nut11/P2PKSecret.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,20 @@ public int getNSigs() {
9494

9595
/**
9696
* NUT-11 refund-path signature threshold. Defaults to {@code 1} when the
97-
* {@code n_sigs_refund} tag is absent — per NUT-11 the refund path requires
98-
* a single signature by default, and existing escrows minted before this
99-
* tag existed must keep their current 1-of-N refund behavior unchanged.
97+
* {@code n_sigs_refund} tag is absent <em>or carries no value</em> — per
98+
* NUT-11 the refund path requires a single signature by default, and
99+
* existing escrows minted before this tag existed must keep their current
100+
* 1-of-N refund behavior unchanged. Guarding the empty-values case also
101+
* stops a malformed secret (key-only tag array) from throwing
102+
* {@link IndexOutOfBoundsException} here.
100103
*/
101104
public int getNSigsRefund() {
102105
Tag tag = super.getTag(P2PKTag.n_sigs_refund.name());
103106
if (tag == null) {
104107
return 1;
105108
}
106-
List<?> values = super.getTag(P2PKTag.n_sigs_refund.name()).getValues();
107-
return values != null ? (int) values.get(0) : 1;
109+
List<?> values = tag.getValues();
110+
return (values != null && !values.isEmpty()) ? (int) values.get(0) : 1;
108111
}
109112

110113
public String getSigFlag() {

cashu-lib-common/src/test/java/xyz/tcheeric/cashu/entities/P2PKSecretTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import xyz.tcheeric.cashu.common.nut10.WellKnownSecret;
77
import xyz.tcheeric.cashu.common.nut11.P2PKSecret;
88

9+
import java.util.List;
10+
911
import static org.assertj.core.api.Assertions.assertThat;
1012

1113
class P2PKSecretTest {
@@ -67,6 +69,22 @@ void shouldSetAndGetNSigsRefund() {
6769
assertThat(secret.getNSigsRefund()).isEqualTo(2);
6870
}
6971

72+
/**
73+
* A malformed secret whose {@code n_sigs_refund} tag carries no value (a key-only tag array,
74+
* which the deserializers accept) must fall back to the default of 1 rather than throwing
75+
* {@link IndexOutOfBoundsException} — otherwise a crafted secret could DoS verification.
76+
*/
77+
@Test
78+
void shouldDefaultNSigsRefundToOneWhenTagHasNoValue() {
79+
// Arrange
80+
byte[] secretData = Hex.decode("deadbeef");
81+
P2PKSecret secret = new P2PKSecret(secretData);
82+
secret.setTag(P2PKSecret.P2PKTag.n_sigs_refund.name(), List.of()); // present but empty
83+
84+
// Act / Assert
85+
assertThat(secret.getNSigsRefund()).isEqualTo(1);
86+
}
87+
7088
/**
7189
* Ensures the n_sigs_refund tag round-trips through JSON serialization the
7290
* same way the existing n_sigs tag does.

0 commit comments

Comments
 (0)