fix(legacy): warn if Ethereum data is too long
What changed, and why it matters
This update changes how older Trezor devices (the 'legacy' T1B1 model) display long Ethereum transaction data before signing. Previously, the device only showed one screen of data, which could hide malicious content appended later in the same transaction. Now it shows up to five pages of data, and if there is still more data left, it shows a clear warning telling the user that not all data can be viewed and to proceed with caution. This is a security improvement that reduces the chance of a user approving a transaction whose full contents they have not seen.
Users of Trezor Model One should update firmware to a version containing this commit. When signing Ethereum transactions with large data fields, verify whether the warning about unviewable data appears and only confirm if the source of the transaction is trusted. Developers should ensure the new button-request sequence is handled correctly by wallet software.
Security signals we found
UI/UX security improvement: warns user when transaction data exceeds displayable length
Mitigates risk of hidden/malicious trailing calldata in Ethereum transactions on legacy devices
Does not change signing logic or data validation; only changes user confirmation flow
Changelog entry explicitly tagged as security: '.changelog.d/222.security'
Evidence from the diff
The patch modifies legacy/firmware/ethereum.c to paginate the display of Ethereum transaction data on legacy Trezor devices. It introduces MAX_DATA_PAGES (5) and loops through up to five pages of 24 bytes each (3 lines of 8 bytes in hex) using a revised layoutEthereumData() that now tracks offset and total length. If data_total exceeds what can be shown in five pages, a warning dialog is presented before signing proceeds. The change also updates tests and UI fixtures to reflect the new button-request sequence.
Changed components
legacy/firmware/ethereum.cTrezor Model One (T1B1) Ethereum signing flowtests/device_tests/ethereum/test_signtx.pytests/ui_tests/fixtures.jsonInspect captured patch +88 / −33
diff --git a/legacy/firmware/.changelog.d/222.security b/legacy/firmware/.changelog.d/222.security
new file mode 100644
index 00000000..e3eb9457
--- /dev/null
+++ b/legacy/firmware/.changelog.d/222.security
@@ -0,0 +1 @@
+Warn if Ethereum data is too long.
diff --git a/legacy/firmware/ethereum.c b/legacy/firmware/ethereum.c
index ec15caa2..d398dbcb 100644
--- a/legacy/firmware/ethereum.c
+++ b/legacy/firmware/ethereum.c
@@ -416,12 +416,32 @@ static void layoutEthereumConfirmTx(const uint8_t *to, uint32_t to_len,
_("Send"), amount.value, _to1, _to2, _to3, NULL);
}
-static void layoutEthereumData(const uint8_t *data, uint32_t len,
- uint32_t total_len) {
+// Format number in decimal, going backwards from `ptr`.
+// The caller must allocate enough memory for the resulting string.
+static inline char *_format_decimal_backwards(char *ptr, uint32_t number) {
+ while (number > 0) {
+ *(--ptr) = '0' + number % 10;
+ number = number / 10;
+ }
+ return ptr;
+}
+
+// Prepend null-terminated string, going backwards from `ptr`.
+// The caller must allocate enough memory for the resulting string.
+static inline char *_prepend_string(char *ptr, const char *str) {
+ size_t len = strlen(str);
+ while (len > 0) {
+ *(--ptr) = str[--len];
+ }
+ return ptr;
+}
+
+static uint32_t layoutEthereumData(const uint8_t *data, uint32_t len,
+ uint32_t total_len, uint32_t offset) {
char hexdata[3][17] = {0};
- char summary[20] = {0};
+ char summary[32] = "... 4294967296/4294967296 bytes";
uint32_t printed = 0;
- for (int i = 0; i < 3; i++) {
+ for (size_t i = 0; i < 3; i++) {
uint32_t linelen = len - printed;
if (linelen > 8) {
linelen = 8;
@@ -430,20 +450,24 @@ static void layoutEthereumData(const uint8_t *data, uint32_t len,
data += linelen;
printed += linelen;
}
+ offset += printed;
- strcpy(summary, "... bytes");
- char *p = summary + 11;
- uint32_t number = total_len;
- while (number > 0) {
- *p-- = '0' + number % 10;
- number = number / 10;
- }
- char *summarystart = summary;
- if (total_len == printed) summarystart = summary + 4;
+ // start from the terminating '\0'
+ char *ptr = summary + sizeof(summary) - 1;
+ ptr = _prepend_string(ptr, " bytes");
+ ptr = _format_decimal_backwards(ptr, total_len);
+ ptr = _prepend_string(ptr, "/");
+ ptr = _format_decimal_backwards(ptr, offset);
+
+ if (offset < total_len) {
+ // add ellipsis if not all data has been shown
+ ptr = _prepend_string(ptr, "... ");
+ }
layoutDialogSwipe(&bmp_icon_question, _("Cancel"), _("Confirm"), NULL,
_("Transaction data:"), hexdata[0], hexdata[1], hexdata[2],
- summarystart, NULL);
+ ptr, NULL);
+ return printed;
}
static void layoutEthereumFee(const uint8_t *value, uint32_t value_len,
@@ -711,6 +735,8 @@ static bool layoutEthereumConfirmStakingTx(const struct signing_params *params,
return true;
}
+static const size_t MAX_DATA_PAGES = 5;
+
static bool ethereum_signing_confirm_common(
const struct signing_params *params) {
enum staking_operation_t staking_op;
@@ -744,11 +770,30 @@ static bool ethereum_signing_confirm_common(
}
if (params->token == NULL && data_total > 0) {
- layoutEthereumData(params->data_initial_chunk_bytes,
- params->data_initial_chunk_size, data_total);
- if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) {
- fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
- return false;
+ const uint8_t *chunk = params->data_initial_chunk_bytes;
+ uint32_t chunk_len = params->data_initial_chunk_size;
+ uint32_t offset = 0;
+
+ for (size_t i = 0; i < MAX_DATA_PAGES && offset < data_total; ++i) {
+ uint32_t confirmed =
+ layoutEthereumData(chunk, chunk_len, data_total, offset);
+ chunk += confirmed;
+ chunk_len -= confirmed;
+ offset += confirmed;
+
+ if (!protectButton(ButtonRequestType_ButtonRequest_SignTx, false)) {
+ fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
+ return false;
+ }
+ }
+ if (offset < data_total) {
+ layoutDialogSwipe(&bmp_icon_warning, _("Abort"), _("Continue"), NULL,
+ _("Warning! Too long"), _("data to view."), NULL,
+ _("Proceed with caution."), NULL, NULL);
+ if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) {
+ fsm_sendFailure(FailureType_Failure_ActionCancelled, NULL);
+ return false;
+ }
}
}
diff --git a/tests/device_tests/ethereum/test_signtx.py b/tests/device_tests/ethereum/test_signtx.py
index b44149db..53e3aa5f 100644
--- a/tests/device_tests/ethereum/test_signtx.py
+++ b/tests/device_tests/ethereum/test_signtx.py
@@ -272,6 +272,11 @@ def test_sanity_checks(session: Session):
)
+# ≤5 pages of data are shown on T1B1.
+# Otherwise, a warning is shown and the rest of the data is skipped.
+LEGACY_MAX_DATA_PAGES = 5
+
+
def test_data_streaming(session: Session):
"""Only verifying the expected responses, the signatures are
checked in vectorized function above.
@@ -283,10 +288,14 @@ def test_data_streaming(session: Session):
is_legacy = client.model in models.LEGACY_MODELS
br_sign_tx = messages.ButtonRequest(code=messages.ButtonRequestType.SignTx)
+ br_protect = messages.ButtonRequest(code=messages.ButtonRequestType.ProtectCall)
expected_responses: list[ExpectedResponse] = [br_sign_tx]
if is_legacy:
- expected_responses += [br_sign_tx, br_sign_tx]
+ expected_responses += [br_sign_tx] * LEGACY_MAX_DATA_PAGES + [
+ br_protect,
+ br_sign_tx,
+ ]
expected_responses.extend(
message_filters.EthereumTxRequest(
diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json
index 51e9269c..1547bcb3 100644
--- a/tests/ui_tests/fixtures.json
+++ b/tests/ui_tests/fixtures.json
@@ -493,7 +493,7 @@
"T1B1_en_ethereum-test_sign_verify_message.py::test_verify[parameters6-result6]": "0f79aea15b09e207f7e2bb226421549a0446a4722dde3000c31c3c66ec2059d1",
"T1B1_en_ethereum-test_sign_verify_message.py::test_verify[parameters7-result7]": "22daa96dbdd3aeeec11f1134baa29c0ccecbadf032a47aaa355f30c994897907",
"T1B1_en_ethereum-test_sign_verify_message.py::test_verify_invalid": "cdec0f79f2abbd90f4346494037f7bb4dd4dccc7c6739b497873d0c5603f2a26",
-"T1B1_en_ethereum-test_signtx.py::test_data_streaming": "8b92d77aa945f6eb426b36c132c2f326adfb753d00d0ce48b5d20db98f5fd8fa",
+"T1B1_en_ethereum-test_signtx.py::test_data_streaming": "9ce5f261f9028efbdded7d1bc35a268f574af9f856e511d9643bd662d7399c46",
"T1B1_en_ethereum-test_signtx.py::test_sanity_checks": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"T1B1_en_ethereum-test_signtx.py::test_sanity_checks_eip1559": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-Auxilium]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
@@ -507,14 +507,14 @@
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-Pirl]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-Unknown_chain_id_eth_path]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-Unknown_chain_id_testnet_path]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
-"T1B1_en_ethereum-test_signtx.py::test_signtx[False-approve_avantis]": "e5a1031099f7bb05f81b1c3c0bff3134f47e7b04e58af4619d46e009fbc97c6b",
-"T1B1_en_ethereum-test_signtx.py::test_signtx[False-data_1]": "4e7857b39f14443fab15a82ac87970111917ba80937c07b33d0c1369d43f4e52",
-"T1B1_en_ethereum-test_signtx.py::test_signtx[False-data_2_bigdata]": "0847b5429bd37576bbde8b252fbd68258d8f971ef5d9d92cf440163f8f7b7b9b",
+"T1B1_en_ethereum-test_signtx.py::test_signtx[False-approve_avantis]": "0455e8fdc8342e689e209905c08f4b741c85116537ec26803f0ba1f10fc6984f",
+"T1B1_en_ethereum-test_signtx.py::test_signtx[False-data_1]": "a1fc6fca61a44a00efc44ad76302c69deceae7de25277670ed5dafbb8d223e24",
+"T1B1_en_ethereum-test_signtx.py::test_signtx[False-data_2_bigdata]": "099d17322c21fa9e9182a3384626678e59b249cd4aa5f79c27b32a894793512e",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-erc20_token]": "0d0afc954ca4e40ef1b4cd4fd1fc301e0ab611b66353ee5210ac6678a87b9b58",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-max_chain_id]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-max_chain_plus_one]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-max_uint64]": "2f42cfe000671941480a4cde329e7562a47f22639a6ac7326f768fa6a38616ed",
-"T1B1_en_ethereum-test_signtx.py::test_signtx[False-newcontract]": "a4a6cafc5f95e2427b894459152ee089fbebffd2bd3c4b82bc5ef732dbc23248",
+"T1B1_en_ethereum-test_signtx.py::test_signtx[False-newcontract]": "b07f848ba17de74f6663f554f9cc789246a8c2f39e6f5293e725729ec6585e2c",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-nodata_1]": "341d14a0a993a11e51a4295c0570b3e5975a6d0cbd8553d92a80b62c02cd5dc4",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-nodata_2_bigvalue]": "bb09aa6c65b3991393062e0061f450c2d19bfc158512f1e9c5c366d18828dbd4",
"T1B1_en_ethereum-test_signtx.py::test_signtx[False-wanchain]": "932413226fc9c7cd2e42a14753c985d858f6cc1123e2cbe45e2523a9330f2626",
@@ -529,29 +529,29 @@
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-Pirl]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-Unknown_chain_id_eth_path]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-Unknown_chain_id_testnet_path]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
-"T1B1_en_ethereum-test_signtx.py::test_signtx[True-approve_avantis]": "e5a1031099f7bb05f81b1c3c0bff3134f47e7b04e58af4619d46e009fbc97c6b",
-"T1B1_en_ethereum-test_signtx.py::test_signtx[True-data_1]": "4e7857b39f14443fab15a82ac87970111917ba80937c07b33d0c1369d43f4e52",
-"T1B1_en_ethereum-test_signtx.py::test_signtx[True-data_2_bigdata]": "0847b5429bd37576bbde8b252fbd68258d8f971ef5d9d92cf440163f8f7b7b9b",
+"T1B1_en_ethereum-test_signtx.py::test_signtx[True-approve_avantis]": "0455e8fdc8342e689e209905c08f4b741c85116537ec26803f0ba1f10fc6984f",
+"T1B1_en_ethereum-test_signtx.py::test_signtx[True-data_1]": "a1fc6fca61a44a00efc44ad76302c69deceae7de25277670ed5dafbb8d223e24",
+"T1B1_en_ethereum-test_signtx.py::test_signtx[True-data_2_bigdata]": "099d17322c21fa9e9182a3384626678e59b249cd4aa5f79c27b32a894793512e",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-erc20_token]": "0d0afc954ca4e40ef1b4cd4fd1fc301e0ab611b66353ee5210ac6678a87b9b58",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-max_chain_id]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-max_chain_plus_one]": "7df4e4dcd5fd6f56f8674ce67df9c872279f5683191a5a002d2c4a7929e6d0be",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-max_uint64]": "2f42cfe000671941480a4cde329e7562a47f22639a6ac7326f768fa6a38616ed",
-"T1B1_en_ethereum-test_signtx.py::test_signtx[True-newcontract]": "a4a6cafc5f95e2427b894459152ee089fbebffd2bd3c4b82bc5ef732dbc23248",
+"T1B1_en_ethereum-test_signtx.py::test_signtx[True-newcontract]": "b07f848ba17de74f6663f554f9cc789246a8c2f39e6f5293e725729ec6585e2c",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-nodata_1]": "341d14a0a993a11e51a4295c0570b3e5975a6d0cbd8553d92a80b62c02cd5dc4",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-nodata_2_bigvalue]": "bb09aa6c65b3991393062e0061f450c2d19bfc158512f1e9c5c366d18828dbd4",
"T1B1_en_ethereum-test_signtx.py::test_signtx[True-wanchain]": "932413226fc9c7cd2e42a14753c985d858f6cc1123e2cbe45e2523a9330f2626",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-Base]": "d37c9bb1645fff4c2790e870d61a7736eb2216b6aa43ca9116cb73da97492ad4",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-Ledger Live legacy path]": "de54dbfe5e5bc666b896f13d4fbfbb0253f5638c084831f57ec49148136427f1",
-"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-data_1]": "d3938b80c77a90ab7adba5b3d4307630c1d1914cf376956643fc874ee7bf0a34",
-"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-data_2_bigdata]": "f14104198c194556a2cdf35c43134ec227f29fe93e7987314edfd97b92db98b9",
+"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-data_1]": "f9c0bc8378f03e6ea13db961f278ae91ed07bb037e95ee9681b0f7f8f436e2bb",
+"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-data_2_bigdata]": "239abd36d6767b0b27d35b0f00b254a1786a1ca5308b09f9864484366ee8ef68",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-erc20]": "5e74b78715def0c7e46ed7dea158c219a423b328b7e3415950700e3b6183deed",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-large_chainid]": "de54dbfe5e5bc666b896f13d4fbfbb0253f5638c084831f57ec49148136427f1",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-long_fees]": "bca02a67af6d833029b6819071af37c7aaf2f6bad0d4ba5599de2a7eedf4a49d",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[False-nodata]": "de54dbfe5e5bc666b896f13d4fbfbb0253f5638c084831f57ec49148136427f1",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-Base]": "d37c9bb1645fff4c2790e870d61a7736eb2216b6aa43ca9116cb73da97492ad4",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-Ledger Live legacy path]": "de54dbfe5e5bc666b896f13d4fbfbb0253f5638c084831f57ec49148136427f1",
-"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-data_1]": "d3938b80c77a90ab7adba5b3d4307630c1d1914cf376956643fc874ee7bf0a34",
-"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-data_2_bigdata]": "f14104198c194556a2cdf35c43134ec227f29fe93e7987314edfd97b92db98b9",
+"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-data_1]": "f9c0bc8378f03e6ea13db961f278ae91ed07bb037e95ee9681b0f7f8f436e2bb",
+"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-data_2_bigdata]": "239abd36d6767b0b27d35b0f00b254a1786a1ca5308b09f9864484366ee8ef68",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-erc20]": "5e74b78715def0c7e46ed7dea158c219a423b328b7e3415950700e3b6183deed",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-large_chainid]": "de54dbfe5e5bc666b896f13d4fbfbb0253f5638c084831f57ec49148136427f1",
"T1B1_en_ethereum-test_signtx.py::test_signtx_eip1559[True-long_fees]": "bca02a67af6d833029b6819071af37c7aaf2f6bad0d4ba5599de2a7eedf4a49d",
Why this scored 51/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.