From 0eddfbd468ee66ee6f3f2334e3ca062eae89ca59 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 9 Aug 2026 03:48:47 +0700 Subject: [PATCH] fix(platform-wallet): reject trailing bytes in persisted asset-lock proof blobs decode_from_slice stops at the value's end without rejecting trailing bytes; the lifecycle blob holds exactly one proof, so a longer payload is corruption, not a valid encoding. Regression test tampers a byte onto the proof bytes and asserts the decode fails. Co-Authored-By: Claude Fable 5 --- .../src/changeset/serde_adapters.rs | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/packages/rs-platform-wallet/src/changeset/serde_adapters.rs b/packages/rs-platform-wallet/src/changeset/serde_adapters.rs index b163db30b6..a4dddbd209 100644 --- a/packages/rs-platform-wallet/src/changeset/serde_adapters.rs +++ b/packages/rs-platform-wallet/src/changeset/serde_adapters.rs @@ -134,10 +134,77 @@ pub mod optional_asset_lock_proof { let bytes = Option::>::deserialize(deserializer)?; bytes .map(|b| { - dpp::bincode::decode_from_slice(&b, dpp::bincode::config::standard()) - .map(|(proof, _)| proof) - .map_err(serde::de::Error::custom) + let (proof, consumed) = + dpp::bincode::decode_from_slice(&b, dpp::bincode::config::standard()) + .map_err(serde::de::Error::custom)?; + // `decode_from_slice` stops at the value's end without + // rejecting trailing bytes — but this blob holds exactly + // one proof, so a longer payload is corruption (or + // smuggled data), not a valid encoding. Fail loudly + // rather than silently dropping the tail. + if consumed != b.len() { + return Err(serde::de::Error::custom(format!( + "asset lock proof blob has {} trailing byte(s)", + b.len() - consumed + ))); + } + Ok(proof) }) .transpose() } } + +#[cfg(test)] +mod optional_asset_lock_proof_tests { + use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use dpp::prelude::AssetLockProof; + + #[derive(serde::Serialize, serde::Deserialize)] + struct Carrier { + #[serde(with = "super::optional_asset_lock_proof")] + proof: Option, + } + + fn chain_proof() -> Option { + Some(AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 411_495, + out_point: dashcore::OutPoint::null(), + })) + } + + /// Exact-length payloads round-trip; a payload with appended bytes + /// is corruption and must fail decode instead of silently dropping + /// the tail. + #[test] + fn rejects_trailing_bytes() { + let encoded = dpp::bincode::serde::encode_to_vec( + &Carrier { + proof: chain_proof(), + }, + dpp::bincode::config::standard(), + ) + .expect("encode"); + let (decoded, _): (Carrier, _) = + dpp::bincode::serde::decode_from_slice(&encoded, dpp::bincode::config::standard()) + .expect("exact payload decodes"); + assert_eq!(decoded.proof, chain_proof()); + + // Corrupt the blob by appending a byte INSIDE the proof bytes: + // re-encode with a tampered inner vec. The adapter serializes + // the proof as Option>, so build that shape directly. + let proof_bytes = + dpp::bincode::encode_to_vec(chain_proof().unwrap(), dpp::bincode::config::standard()) + .expect("proof bytes"); + let mut padded = proof_bytes; + padded.push(0xAA); + let tampered = + dpp::bincode::serde::encode_to_vec(&(Some(padded),), dpp::bincode::config::standard()) + .expect("encode tampered carrier"); + let result: Result<(Carrier, _), _> = + dpp::bincode::serde::decode_from_slice(&tampered, dpp::bincode::config::standard()); + assert!( + result.is_err(), + "a proof blob with trailing bytes must be rejected" + ); + } +}