fix(stellar): properly bound string length
What changed, and why it matters
This commit fixes a length-check bug in Trezor's Stellar cryptocurrency support. The device was measuring string length in characters (Unicode code points) instead of bytes. Because some characters use multiple bytes, a string could pass the old check yet be too long for the Stellar protocol, potentially causing malformed transaction data or unexpected device behavior when signing.
Treat as a low-to-moderate security fix. Users should update firmware to a version including this commit. Review whether any other Stellar string fields use len(str) for byte-length limits, and add regression tests covering multi-byte UTF-8 inputs at boundary values.
Security signals we found
Incorrect input validation: length check used code-point count instead of byte count
Protocol compliance mismatch with RFC 4506 XDR string encoding
Potential to generate malformed or oversized Stellar transaction fields
Boundary/length validation bypass via multi-byte UTF-8 characters
Evidence from the diff
The patch changes Stellar string serialization to validate lengths in bytes rather than code points. write_string() now returns len(buf) after UTF-8 encoding, and callers in serialize.py and sign_tx.py compare that byte count against protocol limits (64 bytes for manage data keys, 32 bytes for home domains, 28 bytes for memo text). Previously they used len(str), which counts Unicode code points and can undercount multi-byte characters. This aligns the implementation with RFC 4506 section 4.11, which defines XDR string
Changed components
core/src/apps/stellar/operations/serialize.pycore/src/apps/stellar/sign_tx.pycore/src/apps/stellar/writers.pyInspect captured patch +11 / −8
diff --git a/core/src/apps/stellar/operations/serialize.py b/core/src/apps/stellar/operations/serialize.py
index 133205ee..d50c4ea3 100644
--- a/core/src/apps/stellar/operations/serialize.py
+++ b/core/src/apps/stellar/operations/serialize.py
@@ -73,9 +73,9 @@ def write_create_passive_sell_offer_op(
def write_manage_data_op(w: Writer, msg: StellarManageDataOp) -> None:
- if len(msg.key) > 64:
+ written = write_string(w, msg.key)
+ if written > 64:
raise ProcessError("Stellar: max length of a key is 64 bytes")
- write_string(w, msg.key)
write_bool(w, bool(msg.value))
if msg.value:
write_string(w, msg.value)
@@ -165,9 +165,9 @@ def write_set_options_op(w: Writer, msg: StellarSetOptionsOp) -> None:
write_bool(w, False)
else:
write_bool(w, True)
- if len(msg.home_domain) > 32:
+ written = write_string(w, msg.home_domain)
+ if written > 32:
raise ProcessError("Stellar: max length of a home domain is 32 bytes")
- write_string(w, msg.home_domain)
# signer
if msg.signer_type is None:
diff --git a/core/src/apps/stellar/sign_tx.py b/core/src/apps/stellar/sign_tx.py
index 92103c25..836484ca 100644
--- a/core/src/apps/stellar/sign_tx.py
+++ b/core/src/apps/stellar/sign_tx.py
@@ -87,9 +87,9 @@ async def sign_tx(msg: StellarSignTx, keychain: Slip21Keychain) -> StellarSigned
# Text: 4 bytes (size) + up to 28 bytes
if memo_text is None:
raise DataError("Stellar: Missing memo text")
- if len(memo_text) > 28:
+ written = writers.write_string(w, memo_text)
+ if written > 28:
raise ProcessError("Stellar: max length of a memo text is 28 bytes")
- writers.write_string(w, memo_text)
memo_confirm_text = memo_text
elif memo_type == StellarMemoType.ID:
# ID: 64 bit unsigned integer
diff --git a/core/src/apps/stellar/writers.py b/core/src/apps/stellar/writers.py
index 783cae20..826923ed 100644
--- a/core/src/apps/stellar/writers.py
+++ b/core/src/apps/stellar/writers.py
@@ -13,8 +13,10 @@ if TYPE_CHECKING:
from trezor.utils import Writer
-def write_string(w: Writer, s: StrOrBytes) -> None:
- """Write XDR string padded to a multiple of 4 bytes."""
+def write_string(w: Writer, s: StrOrBytes) -> int:
+ """Write XDR string padded to a multiple of 4 bytes.
+ Returns the length of the string written to the buffer (without padding).
+ """
# NOTE: 2 bytes smaller than if-else
buf = s.encode() if isinstance(s, str) else s
write_uint32(w, len(buf))
@@ -23,6 +25,7 @@ def write_string(w: Writer, s: StrOrBytes) -> None:
remainder = len(buf) % 4
if remainder:
writers.write_bytes_unchecked(w, bytes([0] * (4 - remainder)))
+ return len(buf)
def write_bool(w: Writer, val: bool) -> None:
Why this scored 60/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.