Merge pull request #661 from Foundation-Devices/fix/seedqr-validation
What changed, and why it matters
This update tightens the checks on SeedQR codes, which are QR codes that encode a wallet's backup seed phrase as numbers. Before, the decoder might accept invalid or oddly-sized numeric strings and either crash or produce an incorrect seed. Now it rejects anything that isn't the right length, isn't all digits, or contains word index numbers that are too high. This protects users from importing a corrupted or attacker-crafted seed that could lead to an unusable or insecure wallet.
Treat this as a security-hardening fix and include it in the next firmware release. Run the new unit tests on target hardware and consider adding integration tests for the camera-based SeedQR scanning path to ensure malformed QR codes are rejected with a clear user-facing error.
Security signals we found
Input validation added to seed-import path
Out-of-range BIP-39 word index now rejected explicitly
Non-digit characters rejected before integer conversion
Length and word-count validation moved ahead of decoding
New unit tests cover valid and malformed SeedQR payloads
Evidence from the diff
The SeedQR decoder now validates input before converting it to BIP-39 words. It rejects payloads whose length is not a multiple of 4, whose resulting word count is not a supported seed length, and any payload containing non-digit characters. It also rejects each 4-digit BIP-39 word index if it is >= SEED_WORD_LIST_LENGTH (2048) and handles the case where trezorcrypto.bip39.get_word() returns None. Previously, validation only occurred after decoding all words and relied on a broad exception handler, so malformed or out-of-range inputs could trigger exceptions or produce invalid seeds silently.
Changed components
ports/stm32/boards/Passport/modules/data_codecs/seedqr_codec.pySeedQR import / restore flowBIP-39 seed phrase decodingInspect captured patch +59 / −6
### ports/stm32/boards/Passport/modules/data_codecs/seedqr_codec.py
@@ -13,7 +13,7 @@
from .data_decoder import DataDecoder
from .data_sampler import DataSampler
from .qr_type import QRType
-from public_constants import SEED_LENGTHS
+from public_constants import SEED_LENGTHS, SEED_WORD_LIST_LENGTH
class SeedQRDecoder(DataDecoder):
@@ -33,16 +33,28 @@ def decode(self, **kwargs):
import trezorcrypto
try:
+ if len(self.data) % 4 != 0:
+ return None
+
+ num_words = len(self.data) // 4
+ if num_words not in SEED_LENGTHS:
+ return None
+
+ if any(c < '0' or c > '9' for c in self.data):
+ return None
+
seed_phrase = []
- num_words = int(len(self.data) / 4)
for i in range(0, num_words):
index = int(self.data[i * 4: (i * 4) + 4])
+ if index >= SEED_WORD_LIST_LENGTH:
+ return None
+
word = trezorcrypto.bip39.get_word(index)
+ if word is None:
+ return None
+
seed_phrase.append(word)
- if len(seed_phrase) in SEED_LENGTHS:
- return seed_phrase
- else:
- return None
+ return seed_phrase
except Exception as e:
return None
### ports/stm32/boards/Passport/modules/tests/test_unit.py
@@ -24,6 +24,10 @@ def test_psbt_multisig_approval(test):
assert test('psbt_multisig_approval.py') == b'OK'
+def test_seedqr_codec(test):
+ assert test('seedqr_codec.py') == b'OK'
+
+
def test_ui(test):
assert test('ui.py') == b'OK'
### ports/stm32/boards/Passport/modules/tests/unit/seedqr_codec.py
@@ -0,0 +1,37 @@
+# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from data_codecs.seedqr_codec import SeedQRDecoder
+
+
+def decode(data):
+ decoder = SeedQRDecoder()
+ decoder.add_data(data)
+ return decoder.decode()
+
+
+valid_12 = '0000' * 12
+valid_24 = '2047' * 24
+
+assert decode(valid_12) == ['abandon'] * 12
+assert decode(valid_24) == ['zoo'] * 24
+
+malformed = (
+ '',
+ '0000' * 13,
+ '2048' + ('0000' * 11),
+ '9999' + ('0000' * 11),
+ 'abcd' + ('0000' * 11),
+ '-001' + ('0000' * 11),
+ ' 001' + ('0000' * 11),
+ '\u0660' * 4 + ('0000' * 11),
+ valid_12 + '0',
+ valid_12 + '000',
+ valid_24 + '0',
+)
+
+for data in malformed:
+ assert decode(data) is None
+
+return_value.write(b'OK')Why this scored 66/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.