psbt: reject trailing data when parsing if WALLY_PSBT_PARSE_FLAG_COMPLETE is used
What changed, and why it matters
This commit tightens how the library reads PSBT (Partially Signed Bitcoin Transaction) data. It adds a new parsing flag that requires the parser to consume the entire input, and turns that flag on automatically when reading base64-encoded PSBTs. Previously, extra bytes tacked onto the end of a PSBT could be silently ignored. The change is defensive: it helps prevent malformed or intentionally padded PSBTs from being accepted as valid.
Review callers that pass raw bytes and may need WALLY_PSBT_PARSE_FLAG_COMPLETE for strict validation. Ensure downstream consumers expecting trailing-byte tolerance are updated. Consider whether the new flag should also be recommended in documentation for security-sensitive PSBT ingestion.
Security signals we found
New strict-input-consumption parse flag added
Base64 decoder now enforces complete input consumption automatically
Trailing bytes after a valid PSBT now rejected in COMPLETE mode
Regression tests added for trailing-byte PSBT v0 and v2
Behavior change in public API parsing functions
Evidence from the diff
The patch introduces WALLY_PSBT_PARSE_FLAG_COMPLETE (0x4). When set, wally_psbt_from_bytes() now returns WALLY_EINVAL if any unparsed bytes remain after a successful parse. wally_psbt_from_base64_n() now ORs this flag in unconditionally, so base64 PSBTs must be exact. Regression test vectors for PSBT v0 and v2 with a trailing 0x00 byte are added to invalid_psbts[] and psbt.json. The C constant set and JS WASM constants are updated accordingly.
Changed components
src/psbt.cinclude/wally_psbt.hsrc/wasm_package/src/const.jsPSBT base64 parsing APIPSBT byte parsing APIInspect captured patch +26 / −5
### include/wally_psbt.h
@@ -29,8 +29,9 @@ extern "C" {
#define WALLY_PSBT_TXMOD_SINGLE 0x4 /* SIGHASH_SINGLE signature is present */
#define WALLY_PSET_TXMOD_RESERVED 0x1 /* Elements: Reserved: not used and ignored if set */
-#define WALLY_PSBT_PARSE_FLAG_STRICT 0x1 /* Parse strictly according to the PSBT/PSET spec */
-#define WALLY_PSBT_PARSE_FLAG_LOOSE 0x2 /* Allow disallowed and missing mandatory fields */
+#define WALLY_PSBT_PARSE_FLAG_STRICT 0x1 /* Parse strictly according to the PSBT/PSET spec */
+#define WALLY_PSBT_PARSE_FLAG_LOOSE 0x2 /* Allow disallowed and missing mandatory fields */
+#define WALLY_PSBT_PARSE_FLAG_COMPLETE 0x4 /* Require parsing to consume the entire input */
/** Include redundant information to match some buggy PSBT implementations */
#define WALLY_PSBT_SERIALIZE_FLAG_REDUNDANT 0x1
### src/ctest/psbts.h
@@ -91,6 +91,9 @@ static const struct psbt_test invalid_psbts[] =
/* PSBT v0 with duplicate 0 length script */
{"cHNidP8BADMBAAAAAREREREREREREREREREREREREfrK3hERERERERERERERfwAAAAD/////AAAAAAAAAQEJAOH1BQAAAAAAAQgBAAEIAwEBUQA=", false, true},
+ /* PSBT v0 with one P2PKH input. Outputs are empty. With trailing byte 0x00 */
+ {"cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAAAA==", false, true},
+
/* PSBT v2 With Global tx version number < 2 */
{"cHNidP8B+wQCAAAAAQIEAQAAAAEDBAECAwQBBAEAAQUBAAA=", false, true},
@@ -103,6 +106,9 @@ static const struct psbt_test invalid_psbts[] =
/* PSBT v2 missing tx version */
{"cHNidP8B+wQCAAAAAQQBAAEFAQAA", false, true},
+ /* PSBT v2 (no inputs, no outputs) with trailing byte 0x00 */
+ {"cHNidP8BAgQCAAAAAQMEAAAAAAEEAQABBQEAAfsEAgAAAAAA", false, true},
+
/* PSBT v3 - invalid version number */
{"cHNidP8B+wQDAAAAAQIEAgAAAAEFAQAA", false, true},
### src/data/psbt.json
@@ -112,6 +112,10 @@
"comment": "PSBT v0 with duplicate 0 length script",
"psbt": "cHNidP8BADMBAAAAAREREREREREREREREREREREREfrK3hERERERERERERERfwAAAAD/////AAAAAAAAAQEJAOH1BQAAAAAAAQgBAAEIAwEBUQA="
},
+ {
+ "comment": "PSBT v0 with one P2PKH input. Outputs are empty. With trailing byte 0x00",
+ "psbt": "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAAAA=="
+ },
{
"comment": "PSBT v2 With Global tx version number < 2",
"psbt": "cHNidP8B+wQCAAAAAQIEAQAAAAEDBAECAwQBBAEAAQUBAAA="
@@ -128,6 +132,10 @@
"comment": "PSBT v2 missing tx version",
"psbt": "cHNidP8B+wQCAAAAAQQBAAEFAQAA"
},
+ {
+ "comment": "PSBT v2 (no inputs, no outputs) with trailing byte 0x00",
+ "psbt": "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQABBQEAAfsEAgAAAAAA"
+ },
{
"comment": "PSBT v3 - invalid version number",
"psbt": "cHNidP8B+wQDAAAAAQIEAgAAAAEFAQAA"
### src/psbt.c
@@ -31,7 +31,9 @@
#define PSBT_ID_ALL_FLAGS (WALLY_PSBT_ID_AS_V2 | WALLY_PSBT_ID_USE_LOCKTIME)
/* All allowed flags for wally_psbt_from_[bytes|base64]() */
-#define PSBT_ALL_PARSE_FLAGS (WALLY_PSBT_PARSE_FLAG_STRICT|WALLY_PSBT_PARSE_FLAG_LOOSE)
+#define PSBT_ALL_PARSE_FLAGS (WALLY_PSBT_PARSE_FLAG_STRICT | \
+ WALLY_PSBT_PARSE_FLAG_LOOSE | \
+ WALLY_PSBT_PARSE_FLAG_COMPLETE)
static const uint8_t PSBT_MAGIC[5] = {'p', 's', 'b', 't', 0xff};
static const uint8_t PSET_MAGIC[5] = {'p', 's', 'e', 't', 0xff};
@@ -2830,6 +2832,8 @@ int wally_psbt_from_bytes(const unsigned char *bytes, size_t len,
if (ret == WALLY_OK && !*cursor)
ret = WALLY_EINVAL; /* Ran out of data */
+ else if (ret == WALLY_OK && *max && (flags & WALLY_PSBT_PARSE_FLAG_COMPLETE))
+ ret = WALLY_EINVAL; /* Trailing data */
if (ret != WALLY_OK) {
wally_psbt_free(*output);
@@ -3523,8 +3527,9 @@ int wally_psbt_from_base64_n(const char *str_in, size_t str_len, uint32_t flags,
goto done;
}
- /* decode the psbt */
- ret = wally_psbt_from_bytes(decoded, written, flags, output);
+ /* Parse the psbt. For base64, require all bytes to be consumed. */
+ ret = wally_psbt_from_bytes(decoded, written,
+ flags | WALLY_PSBT_PARSE_FLAG_COMPLETE, output);
done:
clear_and_free(decoded, max_len);
### src/wasm_package/src/const.js
@@ -165,6 +165,7 @@ export const WALLY_PSBT_ID_AS_V2 = 0x1; /* Compute PSBT v0 IDs like v2 by settin
export const WALLY_PSBT_ID_BIP370 = 0x0; /* BIP370 compatible */
export const WALLY_PSBT_ID_USE_LOCKTIME = 0x2; /* Do not set locktime to 0 before calculating id */
export const WALLY_PSBT_INIT_PSET = 0x1;
+export const WALLY_PSBT_PARSE_FLAG_COMPLETE = 0x4; /* Require parsing to consume the entire input */
export const WALLY_PSBT_PARSE_FLAG_LOOSE = 0x2; /* Allow disallowed and missing mandatory fields */
export const WALLY_PSBT_PARSE_FLAG_STRICT = 0x1; /* Parse strictly according to the PSBT/PSET spec */
export const WALLY_PSBT_PROPRIETARY_TYPE = 0xFC;Why this scored 51/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.