What changed, and why it matters
This commit fixes a small bug in the COLDCARD wallet's login security check. When the device asks the user to confirm the first and last seed words (to prove they know the backup), the old code could accidentally keep leftover words from a previous attempt. The change makes the word collection more reliable and adds a 'Startup...' message so the screen doesn't look frozen. It is a cleanup/hardening fix rather than a clear-cut exploitable vulnerability.
Treat as a minor hardening fix. Review whether cls.words is reset before each challenge invocation and confirm the new helper does not regress the QWERTY path (seed_word_entry) or the non-QWERTY path. No urgent action required absent additional context.
Security signals we found
Refactor of authentication/seed-word challenge flow
Class-level mutable state (cls.words) reused across calls
Old callback appended words without explicit reset inside the new helper
Potential for stale words to influence challenge outcome
No explicit CVE, advisory, or security disclosure language in commit
Evidence from the diff
The patch refactors WordNestMenu.word entry in shared/seed.py. Previously, login_sequence_word_check() used a class-level menu_done_cbf that appended to cls.words and relied on the caller to clear state. The new get_n_words(nwords) creates a fresh local callback, passes nwords explicitly, and returns cls.words. In shared/ccc.py, sssp_word_challenge() now calls WordNestMenu.get_n_words(2) instead of login_sequence_word_check(), and calls login_now() with await. A dis.fullscreen(‘Startup…’) was added in actions.py after the word challenge. The main risk addressed is state reuse/carry-over of cls.words between attempts or invocations, which could cause incorrect success/failure decisions in the seed-word login challenge.
Changed components
shared/seed.py:WordNestMenu.get_n_words / menu_done_cbfshared/ccc.py:sssp_word_challengeshared/actions.py:start_login_sequenceInspect captured patch +23 / −20
diff --git a/shared/actions.py b/shared/actions.py
index 6aed184..a644df1 100644
--- a/shared/actions.py
+++ b/shared/actions.py
@@ -880,6 +880,7 @@ async def start_login_sequence():
if sp_unlock and sssp_spending_policy('words'):
# challenge them also for first and last seed word! (will reboot on fail)
await sssp_word_challenge()
+ dis.fullscreen("Startup...")
if sp_unlock:
# Disable spending policy going forward; user has to re-enable.
diff --git a/shared/ccc.py b/shared/ccc.py
index cf9f412..2222565 100644
--- a/shared/ccc.py
+++ b/shared/ccc.py
@@ -1129,7 +1129,6 @@ async def sssp_word_challenge(*a):
want_words = words[:1] + words[-1:]
assert len(want_words) == 2
- got_words = []
for retry in range(2):
if version.has_qwerty:
# see special rendering code for this case in ux_q1.py:ux_draw_words(num_words=2)
@@ -1137,18 +1136,17 @@ async def sssp_word_challenge(*a):
got_words = await seed_word_entry('First and Last Seed Words', 2, has_checksum=False)
else:
from seed import WordNestMenu
- got_words = await WordNestMenu.login_sequence_word_check()
+ got_words = await WordNestMenu.get_n_words(2)
if got_words == want_words:
# success - done
return
await ux_show_story("Sorry, those words are incorrect.")
- got_words = []
# they failed; log them out ... they can just try login again
from actions import login_now
- login_now()
+ await login_now()
# NOT-REACHED
diff --git a/shared/seed.py b/shared/seed.py
index 26f6181..eeb1054 100644
--- a/shared/seed.py
+++ b/shared/seed.py
@@ -171,26 +171,30 @@ class WordNestMenu(MenuSystem):
super(WordNestMenu, self).__init__(items)
@classmethod
- async def menu_done_cbf(cls, a, b, c):
- if c.label[-1] == '-':
- lc = c.label[0:-1]
- else:
- lc = ""
- cls.words.append(c.label)
- if len(cls.words) >= 2:
- from glob import numpad
- numpad.abort_ux()
- return
+ async def get_n_words(cls, nwords):
+ # Just block until N words are provided. May only work before menus start?
+ from glob import numpad
+
+ async def menu_done_cbf(menu, b, c):
+ # duplicates some of the logic of next_menu
+ if c.label[-1] == '-':
+ lc = c.label[0:-1]
+ else:
+ lc = ""
+ cls.words.append(c.label)
+ if len(cls.words) >= nwords:
+ numpad.abort_ux()
+ return
- m = cls(prefix=lc, menu_cbf=cls.menu_done_cbf)
- the_ux.push(m)
- await m.interact()
+ m = cls(prefix=lc, menu_cbf=menu_done_cbf)
+ the_ux.push(m)
+ await m.interact()
+
+ m = cls(num_words=nwords, menu_cbf=menu_done_cbf, has_checksum=False)
- @classmethod
- async def login_sequence_word_check(cls):
- m = cls(num_words=2, menu_cbf=WordNestMenu.menu_done_cbf)
the_ux.push(m)
await the_ux.interact()
+
return cls.words
@staticmethod
Why this scored 26/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.