Skip to content

Reject malformed signmessage QR data instead of raising - #968

Open
ruipereira1 wants to merge 1 commit into
SeedSigner:devfrom
ruipereira1:fix/signmessage-qr-bounds
Open

Reject malformed signmessage QR data instead of raising#968
ruipereira1 wants to merge 1 commit into
SeedSigner:devfrom
ruipereira1:fix/signmessage-qr-bounds

Conversation

@ruipereira1

@ruipereira1 ruipereira1 commented Aug 1, 2026

Copy link
Copy Markdown

Description

Problem or Issue being addressed

SignMessageQrDecoder.add() indexes into the split payload with no bounds checks:

parts = segment.split()
self.derivation_path = parts[1].replace("h", "'")
fmt = parts[2].split(":")[0]
self.message = segment.split(f"{fmt}:")[1]

All three of these raise IndexError:

signmessage
signmessage m/84h/0h/0h/0/0
signmessage m/84h/0h/0h/0/0 nocolon

detect_segment_type() routes anything starting with signmessage here, so a QR
containing just that one word is enough to trigger it.

ScanScreen._run() (gui/screens/scan_screens.py:239) calls
self.decoder.add_image(frame) with no exception handling, so the error unwinds to
Controller.handle_exceptionUnhandledExceptionView. The user sees
System Error / IndexError instead of the intended "Unknown QR Type" warning, and the
camera's video stream is left running — ScanScreen only calls
camera.stop_video_stream_mode() on its normal exit paths, and BaseScreen.display()'s
finally stops threads but not the camera. It self-heals on the next scan, since
start_video_stream_mode() stops an existing stream first.

Separately, segment.split(f"{fmt}:")[1] splits on every occurrence and takes index 1,
so a message that itself contains ascii: is silently truncated at the second occurrence.

Solution

split(maxsplit=2) plus partition(":"), with explicit DecodeQRStatus.INVALID returns
and no assignment to self before validation succeeds. The truncation is fixed as a side
effect, since only the first colon now separates format from payload.

Additional Information

Two follow-ups I deliberately kept out of scope, happy to submit either if you agree with
the approach:

  • Wrapping the add_image() call in ScanScreen._run() so that no decoder can take down
    the scan flow.
  • Moving camera.stop_video_stream_mode() into a finally.

Note also that this does not fix every crash in the sign-message flow: the decoder
validates the number of fields, not the shape of the derivation path, and
embit_utils.parse_derivation_path() raises IndexError on paths with fewer than three
components. I've reported that separately.

Screenshots

No new or modified screens — a malformed payload now reaches the existing
ScanInvalidQRTypeView instead of UnhandledExceptionView. Both are unchanged.


This pull request is categorized as a:

  • New feature
  • Bug fix
  • Code refactor
  • Documentation
  • Other

Checklist

I ran pytest locally

  • All tests passed before submitting the PR
  • I couldn't run the tests
  • N/A

188 passed with this change. 4 tests in test_seedqr.py that decode QR images could not run in my environment because libzbar does not load on Windows; they fail identically on an unmodified dev checkout (baseline: 184 passed, same 4 failures). They pass in CI.


I included screenshots of any new or modified screens

  • Yes
  • No
  • N/A

I added or updated tests

  • Yes
  • No, I'm a fool
  • N/A

New file: tests/test_signmessage_qr_decoder.py


I tested this PR hands-on on the following platform(s):

  • Raspberry Pi OS Manual Build
  • SeedSigner OS on a Pi0/Pi0W board
  • Emulator

Not tested on hardware or the emulator — I don't have a device. Covered by unit tests against the decoder.


I have reviewed these notes:

  • I understand

SignMessageQrDecoder.add() indexed parts[1] and parts[2] unconditionally and
then split on the format separator without checking that it exists. Any of
these payloads raises IndexError:

    signmessage
    signmessage m/84h/0h/0h/0/0
    signmessage m/84h/0h/0h/0/0 nocolon

detect_segment_type() classifies anything starting with "signmessage" as
SIGN_MESSAGE, so a QR containing just that word is enough to trigger it.
ScanScreen._run() calls decoder.add_image() with no exception handling, so the
error propagates to Controller.handle_exception and the user lands on the
generic "System Error" screen rather than the "Unknown QR Type" warning. The
camera's video stream is also left running, since ScanScreen only stops it on
its normal exit paths.

The scanner is the device's only untrusted input; no QR payload should be able
to raise out of the decoder.

Also splits with maxsplit=2 and partitions on the first colon, so a message
that itself contains "ascii:" is no longer silently truncated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants