stellar-xdr: `StringM::from_str` bypasses max length validation
Description
stellar-xdr is a library and CLI containing types and functionality for working with Stellar XDR. Prior to version 25.0.1, StringM::from_str does not validate that the input length is within the declared maximum (MAX). Calling StringM::::from_str(s) where s is longer than N bytes succeeds and returns an Ok value instead of Err(Error::LengthExceedsMax), producing a StringM that violates its length invariant. This affects any code that constructs StringM values from string input using FromStr (including str::parse), and relies on the type's maximum length constraint being enforced. An oversized StringM could propagate through serialization, validation, or other logic that assumes the invariant holds. This issue has been patched in version 25.0.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2026-29795 bypasses the MAX length invariant in stellar-xdr's StringM type via FromStr, allowing oversized strings to propagate and affect downstream validation or serialization logic.
Vulnerability
Details
CVE-2026-29795 is a validation bypass vulnerability in the stellar-xdr Rust library (versions ≤25.0.0). The StringM::from_str implementation fails to validate that the input string's byte length does not exceed the type's declared maximum (MAX). Calling StringM::::from_str(s) with a string longer than N bytes incorrectly returns Ok(Self(b)) instead of Err(Error::LengthExceedsMax). This produces a StringM instance that violates the type's length invariant [1][4].
Exploitation and
Attack Surface
Exploitation does not require authentication; any code path that constructs a StringM value from untrusted string input via FromStr (including str::parse) is affected. Attackers can supply an oversized string to trigger the invalid state. The flaw is reachable through any XDR parsing or processing that relies on the StringM type's constraints, potentially across network boundaries or when handling external data [1][4].
Impact
An attacker can inject a StringM value that exceeds the allowed length, which may then propagate through serialization, validation, or other application logic that assumes the length invariant holds. This can lead to unexpected behavior such as memory corruption, denial of service, or inadvertent data truncation/overflow in downstream consumers. The exact impact depends on how the invalid StringM is processed by the larger system [1].
Mitigation
The issue is patched in stellar-xdr version 25.0.1, where the fix (merged in pull request #500) replaces the direct Ok(Self(b)) construction with b.try_into(), which properly validates length via TryFrom<Vec> [2][4]. Until upgraded, users should either validate the byte length of input before calling from_str, or use StringM::try_from(s.as_bytes().to_vec()) which correctly enforces the constraint [4].
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
stellar-xdrcrates.io | < 25.0.1 | 25.0.1 |
Affected products
2- Range: <25.0.1
- stellar/rs-stellar-xdrv5Range: < 25.0.1
Patches
11f840013c3e2Fix StringM::from_str max length validation (#500)
4 files changed · +71 −14
src/curr/generated.rs+1 −1 modified@@ -1976,7 +1976,7 @@ impl<const MAX: u32> core::str::FromStr for StringM<MAX> { type Err = Error; fn from_str(s: &str) -> core::result::Result<Self, Self::Err> { let b = escape_bytes::unescape(s.as_bytes()).map_err(|_| Error::Invalid)?; - Ok(Self(b)) + b.try_into() } }
src/next/generated.rs+1 −1 modified@@ -1976,7 +1976,7 @@ impl<const MAX: u32> core::str::FromStr for StringM<MAX> { type Err = Error; fn from_str(s: &str) -> core::result::Result<Self, Self::Err> { let b = escape_bytes::unescape(s.as_bytes()).map_err(|_| Error::Invalid)?; - Ok(Self(b)) + b.try_into() } }
tests/stringm.rs+43 −0 added@@ -0,0 +1,43 @@ +#![cfg(all( + any(feature = "curr", feature = "next"), + not(all(feature = "curr", feature = "next")) +))] +#![cfg(feature = "std")] + +#[cfg(feature = "curr")] +use stellar_xdr::curr as stellar_xdr; +#[cfg(feature = "next")] +use stellar_xdr::next as stellar_xdr; + +use stellar_xdr::{Error, StringM}; + +use std::str::FromStr; + +#[test] +fn stringm_from_str_at_max() { + // Exactly at the limit should succeed. + let result = StringM::<3>::from_str("abc"); + assert!(result.is_ok()); + assert_eq!(result.unwrap().as_vec(), b"abc"); +} + +#[test] +fn stringm_from_str_within_max() { + // Within the limit should succeed. + let result = StringM::<3>::from_str("ab"); + assert!(result.is_ok()); + assert_eq!(result.unwrap().as_vec(), b"ab"); +} + +#[test] +fn stringm_from_str_exceeding_max() { + let result = StringM::<3>::from_str("abcd"); + assert_eq!(result, Err(Error::LengthExceedsMax)); +} + +#[test] +fn stringm_from_str_empty() { + let result = StringM::<3>::from_str(""); + assert!(result.is_ok()); + assert_eq!(result.unwrap().as_vec(), b""); +}
xdr-generator/generator/header.rs+26 −12 modified@@ -33,10 +33,10 @@ use std::string::FromUtf8Error; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; -#[cfg(all(feature = "schemars", feature = "alloc", feature = "std"))] -use std::borrow::Cow; #[cfg(all(feature = "schemars", feature = "alloc", not(feature = "std")))] use alloc::borrow::Cow; +#[cfg(all(feature = "schemars", feature = "alloc", feature = "std"))] +use std::borrow::Cow; // TODO: Add support for read/write xdr fns when std not available. @@ -72,11 +72,11 @@ pub enum Error { impl PartialEq for Error { fn eq(&self, other: &Self) -> bool { match (self, other) { - (Self::Invalid, Self::Invalid) - | (Self::Unsupported, Self::Unsupported) + (Self::Invalid, Self::Invalid) + | (Self::Unsupported, Self::Unsupported) | (Self::LengthExceedsMax, Self::LengthExceedsMax) - | (Self::LengthMismatch, Self::LengthMismatch) - | (Self::NonZeroPadding, Self::NonZeroPadding) => true, + | (Self::LengthMismatch, Self::LengthMismatch) + | (Self::NonZeroPadding, Self::NonZeroPadding) => true, (Self::Utf8Error(l), Self::Utf8Error(r)) => l == r, @@ -605,7 +605,7 @@ where base64::engine::general_purpose::GeneralPurpose, SkipWhitespace<&mut R>, >, - Self + Self, > { let dec = base64::read::DecoderReader::new( SkipWhitespace::new(&mut r.inner), @@ -940,7 +940,11 @@ impl<T: WriteXdr, const N: usize> WriteXdr for [T; N] { #[cfg(feature = "alloc")] #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", serde_with::serde_as, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize) +)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct VecM<T, const MAX: u32 = { u32::MAX }>(Vec<T>); @@ -1022,7 +1026,11 @@ where where S: serde::Serializer, { - serializer.collect_seq(source.iter().map(|item| serde_with::ser::SerializeAsWrap::<T, U>::new(item))) + serializer.collect_seq( + source + .iter() + .map(|item| serde_with::ser::SerializeAsWrap::<T, U>::new(item)), + ) } } @@ -1895,7 +1903,7 @@ impl<const MAX: u32> core::str::FromStr for StringM<MAX> { type Err = Error; fn from_str(s: &str) -> core::result::Result<Self, Self::Err> { let b = escape_bytes::unescape(s.as_bytes()).map_err(|_| Error::Invalid)?; - Ok(Self(b)) + b.try_into() } } @@ -3171,7 +3179,10 @@ mod tests_for_number_or_string { fn deserialize_i64_from_json_reader() { let json = r#"{"val": "123"}"#; let expected = TestI64 { val: 123 }; - assert_eq!(serde_json::from_reader::<_, TestI64>(Cursor::new(json)).unwrap(), expected); + assert_eq!( + serde_json::from_reader::<_, TestI64>(Cursor::new(json)).unwrap(), + expected + ); } #[test] @@ -3470,7 +3481,10 @@ mod tests_for_number_or_string { fn deserialize_u64_from_json_reader() { let json = r#"{"val": "123"}"#; let expected = TestU64 { val: 123 }; - assert_eq!(serde_json::from_reader::<_, TestU64>(Cursor::new(json)).unwrap(), expected); + assert_eq!( + serde_json::from_reader::<_, TestU64>(Cursor::new(json)).unwrap(), + expected + ); } #[test]
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-x57h-xx53-v53wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-29795ghsaADVISORY
- github.com/stellar/rs-stellar-xdr/commit/1f840013c3e2fca0321fb844b048afa01d10dda6ghsax_refsource_MISCWEB
- github.com/stellar/rs-stellar-xdr/issues/499ghsax_refsource_MISCWEB
- github.com/stellar/rs-stellar-xdr/pull/500ghsax_refsource_MISCWEB
- github.com/stellar/rs-stellar-xdr/security/advisories/GHSA-x57h-xx53-v53wghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.