descriptor: reject nested expressions with unconsumed trailing input
What changed, and why it matters
This commit fixes a parsing bug in libwally-core's Bitcoin descriptor handling. Previously, if extra characters appeared after a valid nested expression inside parentheses, the parser silently ignored them. For example, 'wsh(pk(KEY)xyz)' was accepted as if it were just 'wsh(pk(KEY))'. This could let malformed or attacker-crafted descriptors be misinterpreted, potentially causing two users or programs to disagree about what a wallet script actually is. The fix makes the parser reject such trailing garbage.
Upgrade to a libwally-core release containing this commit. Review any previously accepted descriptor strings for trailing content, especially those generated from untrusted input or imported from third parties. If descriptors are used to derive addresses or scripts, re-validate them with the patched parser and compare the resulting scripts.
Security signals we found
Input validation bypass: parser accepted malformed descriptors with trailing garbage
Silent truncation/misinterpretation of user-supplied descriptor strings
Potential for consensus or policy mismatch between systems parsing the same descriptor
No explicit CVE or vendor security advisory supplied in commit or references
Evidence from the diff
In src/descriptor.c, analyze_miniscript() now checks that a nested expression consumes its entire input string. If a nested builtin node has leftover, unconsumed trailing characters after parsing, it returns WALLY_EINVAL instead of WALLY_OK. The top-level descriptor remains delimited by its checksum and is not affected by this stricter check. Tests were added for trailing garbage in wsh(…), sh(wsh(…)), and inner miniscript expressions.
Changed components
src/descriptor.canalyze_miniscript()Descriptor parsing for nested miniscript expressionsTest suite in src/ctest/test_descriptor.cInspect captured patch +17 / −0
diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c
index b2b0865..34457e3 100644
--- a/src/ctest/test_descriptor.c
+++ b/src/ctest/test_descriptor.c
@@ -1267,6 +1267,18 @@ static const struct descriptor_test {
"descriptor - sh-wsh - non-B top level miniscript",
"sh(wsh(v:pk(key_1)))",
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "", VARS_STD
+ },{
+ "descriptor - wsh - trailing garbage in nested expression",
+ "wsh(pk(key_1)garbage)",
+ WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "", VARS_STD
+ },{
+ "descriptor - sh - trailing garbage in nested expression",
+ "sh(wsh(pk(key_1)garbage))",
+ WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "", VARS_STD
+ },{
+ "descriptor - wsh - trailing garbage in inner expression",
+ "wsh(and_v(v:pk(key_1),pk(key_2)garbage))",
+ WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "", VARS_STD
},{
"descriptor - pk - non-key child",
"pk(1)",
diff --git a/src/descriptor.c b/src/descriptor.c
index ff9e282..7c8aaf1 100644
--- a/src/descriptor.c
+++ b/src/descriptor.c
@@ -2687,6 +2687,11 @@ static int analyze_miniscript(ms_ctx *ctx, const char *str, size_t str_len,
}
}
+ /* A nested expression must consume its entire input. The top
+ * level is delimited by its checksum instead. */
+ if (ret == WALLY_OK && parent && node->builtin && offset != str_len)
+ ret = WALLY_EINVAL;
+
if (ret == WALLY_OK && !seen_indent) {
/* A constant value. Parse it ignoring any already added wrappers */
offset = node->wrapper_str[0] ? strlen(node->wrapper_str) + 1 : 0;
Why this scored 53/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.