TL;DR
Ledger's Ethereum app 1.22.2 carries the date August 12, 2026 in its own changelog, where the entire entry reads "Security issues." The release tag was cut the following day. Ten days after that changelog date, the security firm TestMachine published a thread claiming every Ledger running the Ethereum app was vulnerable to signature substitution: a malicious dApp with WebHID access could race a second APDU into a transaction review and swap the payload being signed while the device screen kept showing the original. Ledger CTO Charles Guillemet called the thread "manufacturing fear for attention" and said his own team had found and fixed the bug two weeks earlier. Both statements are defensible. The git history between the two tags contains twenty-one fix commits authored between August 6 and August 13, and their messages describe the defect in more detail than either party gave the public. No funds are known to have moved. No CVE was assigned and no Ledger security bulletin was published. A hardware wallet screen is a security boundary only while the firmware refuses to change what sits behind it.
What Actually Shipped in Ethereum App 1.22.2?
Twenty-one fixes, spread across the app's whole command surface.
Ledger develops the Ethereum app in the open, so the fix is readable even though the writeup is not. GitHub's compare API for the 1.22.1 and 1.22.2 tags reports twenty-seven commits across sixty-four files. Strip the release merge, a CI workflow change, two icon fixes, a screenshot refresh and the version bump, and twenty-one are substantive fixes, all authored by a single Ledger engineer between August 6 and August 13, with fifteen new unit-test files adding roughly 6,600 lines by the API's own per-file counts. We cite the API because this range is large enough that GitHub declines to render it in the web view, and every commit timestamp below comes from that same response.
The first commit in that range is the one that matters. Reject signing commands that arrive while a review is pending states the old behavior plainly:
INS 0x04 (sign TX), INS 0x08 (sign personal message) and INS 0x0C (sign EIP-712 v0) each called
reset_app_context()orset_idle()when appState was not idle, silently tearing down an active review before returning an error.
Read the diff and the three handlers failed differently. The personal-message handler already refused the second command, but called set_idle() on the way out, killing the review the user was still looking at. The other two did something worse. Both ran reset_app_context() and then kept going:
// src/features/sign_tx/cmd_sign_tx.c, before 1.22.2
if (appState != APP_STATE_IDLE) {
reset_app_context();
}
appState = APP_STATE_SIGNING_TX;That is the whole primitive. A second P1_FIRST chunk wiped the in-flight signing context and installed a fresh one from the attacker's payload, and the function returned success. The fix replaces reset_app_context() with return SWO_COMMAND_NOT_ALLOWED in both handlers. Three lines.
What Is Clear Signing, and What Did the Race Break?
Clear signing is the promise that the screen is the transaction.
Ledger defines it as turning "the raw, encoded data of a blockchain transaction into plain language that a person can read and verify before approving it," and adds that it "never modifies the transaction; it adds a display layer that is verified on the signer itself." The mechanism is ERC-7730, an open metadata format mapping contract functions and EIP-712 messages to human-readable labels so the device renders "Approve spending" rather than a wall of calldata. The alternative is blind signing, confirming a hash you cannot interpret. Ledger's product argument for a decade has been that the second screen exists so the first one can lie.
The race broke the binding between the two. The display came from one context and the signature from another, with a window in between where the host could substitute the second. The user's check and the device's use were separated in time, which is CWE-367, a time-of-check time-of-use race condition, a child of CWE-362. The unusual part is where the check lives. In most TOCTOU bugs both halves sit inside the program. Here the check is a human reading a screen, and the state they checked was freeable memory.
The screen survived the swap because nothing dismissed it. A companion commit, Fix appState and idle handling around reset_app_context(), had to add a main-loop step to "dismiss an ongoing review before the reset frees its UI buffers." Before that, the reset freed the buffers and left the review on the glass.
The Attack, Step by Step
The precondition is a WebHID connection, which any dApp you have clicked "connect" for already holds in that browser tab. No physical access, no supply chain compromise. The attacker needs the transport and a few hundred milliseconds of your reading time.
| # | Actor | Action | Evidence |
|---|---|---|---|
| 1 | User | Grants a page WebHID access to the device and initiates a normal transaction | Reported |
| 2 | Host page | Sends INS 0x04 P1_FIRST, the real transaction | App source |
| 3 | Device | Parses it, renders the clear-signed review, waits for a button press | App source |
| 4 | Host page | Sends a second INS 0x04 P1_FIRST carrying the attacker's payload | Commit 4306b7a9 |
| 5 | Device | appState != IDLE, so it calls reset_app_context() and continues into a new signing session | Commit 4306b7a9 |
| 6 | Screen | Still shows transaction one, because nothing dismissed the review | Commit 6fa474f3 |
| 7 | User | Reads transaction one and presses approve | Reported |
| 8 | Device | Approval callback signs without checking that the state still matches | Commit 18a78708 |
| 9 | Host page | Receives a valid signature over transaction two | Commit 18a78708 |
The path is not a straight line, because two parties act at once. The host keeps talking on the APDU channel during the exact window in which the device is waiting on a human.
Step 8 turns a display glitch into a theft. Validate appState in signing approval callbacks records that "the four approval callbacks (TX, EIP-191, EIP-712, EIP-7702) signed and returned a signature without verifying that the application was still in the expected signing state." The button was wired straight to the key.
No source describes how a victim reaches the malicious page, and that gap is honest rather than lazy: the flaw was closed before anyone used it, so there is no delivery chain to reconstruct. Both routes the industry has already seen would work. One is a cloned frontend on a lookalike or lapsed domain, which is how 810 ETH left Tornado Cash last week. The other is a real dApp whose frontend or a dependency has been compromised. Neither asks the victim to do anything unusual, because granting WebHID access is the ordinary first step of using a Ledger in a browser.
The Bug Behind the Bug
The transaction handler was the headline. Four other command paths had the same hole.
The EIP-712 v1 lock commit is the most alarming thing in the range and describes a failure mode nobody has covered. appState was only set to APP_STATE_SIGNING_EIP712 inside ui_712_start(), which runs when the UI is about to appear. Every STRUCT_DEF, STRUCT_IMPL and FILTERING command before that ran with no guard at all. A host could interleave a complete EIP-712 sequence into an active review, and the sign handler would then, in Ledger's own words, "call reset_app_context() (destroying the active review and zeroing tmpCtx immediately after td_hash_pass() had written the domain and message hashes), then present an approval screen over zeroed data."
An approval screen over zeroed data. The device offered to sign a structure whose hashes had just been erased underneath it.
Address verification was exposed too, which is the flow people are told to trust most. Guard address verification against concurrent signing commands notes that during the confirm flow appState stayed idle, so an interleaved EIP-712 command could call ui_712_init() and explicit_bzero the shared strings union "while NBGL was still rendering strings.common.toAddress from it." The receive-address screen, the one that exists to defeat clipboard malware, read from a buffer another command could blank mid-draw.
A third command let the host choose the display itself. Pin the signing mode (P2) to the value set at P1_FIRST records that handle_sign() read the display mode from whichever chunk was current, so a host could open a stream in store mode with no UI and close it in basic mode, "silently changing the flow's behavior on the last chunk." The follow-up commit is candid that the pinning already covered the remaining case and that there was "no functional impact." Flagging its own cleanup instead of folding it into the security count is the kind of note that makes the rest of the range credible.
The sweep continued outward: the privacy-operation command overwrote the shared context through a union with no state check, the gating command could be issued in any state, a field command arriving mid-review freed buffers the renderer still pointed at and crashed the device. April's Ethereum app 1.22.0 release had already fixed two separate blind-signing bypasses. This app's problem is not a single mistake.
Who Found It, and Why the Timeline Is Contested
TestMachine says its agent Azimuth found the flaw in an autonomous scan, validated it on a Ledger Flex, and that the same APDU and UI code ships on Nano X, Nano S Plus, Stax and Apex, per BeInCrypto's account. The 1.22.2 test snapshots cover flex, stax and apex_p, consistent with that claim. By the same account, TestMachine reached Ledger's bounty program only after the patch had shipped, then turned down the reward and praised the speed of the fix. It has not said publicly why it declined. Guillemet's response was that the issue "was fixed and deployed two weeks ago," that TestMachine "reached out to our bounty program after the fix was already shipped, and did not follow responsible disclosure," and that publishing a thread implying the problem was unsolved is "not security research."
The public record supports most of both accounts.
| When (UTC) | Event | Source |
|---|---|---|
| 2026-05-27 | 1.22.1 released, fixing "instability in APDU communication handling" | Changelog |
| 2026-08-06 13:19 | First fail-closed guard commit authored | GitHub API |
| 2026-08-07 to 08-13 | Remaining twenty guard and hardening commits authored | GitHub API |
| 2026-08-12 10:00 | Version bump authored; changelog dates the release 2026-08-12 | GitHub API |
| 2026-08-13 09:54 | Whole series committed onto the release branch and tagged 1.22.2 | GitHub API |
| 2026-08-22 00:11 | TestMachine publishes the signature-substitution thread | X |
| 2026-08-23 | Guillemet responds publicly | Reported |
| 2026-08-24 12:35 | GitHub release entry for 1.22.2 becomes public | GitHub API |
Ledger fixed it first. The dates settle that: the guard work began on August 6, sixteen days before the thread, and the tag exists as of August 13. Guillemet's "two weeks" is nine days measured against the thread, close enough for a CTO writing at speed.
Nobody was told. The changelog says "Security issues." The Donjon bulletin index runs to twenty-two numbered advisories and stops at a Monero key-recovery issue from June 4, 2026. This one did not make it twenty-three. There is no CVE either: a keyword search of the NVD API returns nothing as of August 25, 2026. A user on 1.22.1 had no way to learn that updating was urgent, and that vacuum is what TestMachine's thread filled.
One detail cuts against the simple reading. The GitHub release entry for 1.22.2 carries a created timestamp of August 13 and a published timestamp of August 24, the day after the argument, which looks like a company posting release notes under pressure. It may not be: the head commit of that same tag replaces the app's release-publishing workflow with a shared reusable one, so a release-automation change explains the eleven-day gap just as well. We cannot separate the two from outside.
Ledger has published no technical account, so whether Donjon's August 6 commits came from the same finding TestMachine reported later, or from a parallel review that landed first, rests on Guillemet's word alone. The chain above is our reading of the commit messages and diffs, corroborated by TestMachine's public description.
Why a Display Bug Is a Signing Bug
This has happened in this exact app before.
On January 13, 2021, Ledger published LSB 015: a regression in Ethereum app 1.6.0 stopped it displaying transaction data for unsupported assets, so a compromised third-party wallet client could get a user to sign fields the device never showed. Same app, same class, five and a half years apart. What you saw was not what you signed.
The class deserves a name, because the industry keeps filing it under UI defects. A signing device makes exactly one guarantee: the bytes it renders are the bytes it authorizes. Everything else, the secure element, the seed handling, the passphrase, protects a key that will still sign whatever the state machine hands it. When render and signing read from a shared context a third party can rewrite, the secure element is a very expensive rubber stamp.
The stakes scale with what the substituted payload can be. In the illustration TestMachine and the coverage both reached for, the swap turns a small transfer into an unlimited token approval, and standing approvals have already moved real money: Chainalysis put approval phishing at more than $1 billion since May 2021 in its 2023 crime research, all of it signed by the victims themselves. That is the mild version. The 1.22.2 commits put an EIP-7702 approval callback in the same unguarded set, and an EIP-7702 authorization points an account's code at a contract of the signer's choosing, code the specification says "has unrestricted access to the account."
There is no smart contract in this story. The vulnerable code is C on an embedded device, the attack surface is a USB protocol, and the failure is a state machine that chose recovery over refusal. Ledger's own tooling, by their account, found it. An external audit scoped to the APDU handler set would have found it too, since every fix in the range is a guard clause. Runtime monitoring sits on the wrong side of the problem. On-chain, the malicious approval looks like a user approving something, because it is. That is the honest limit of our own product line: monitoring cannot see a firmware race, only the allowance it produces on an address you told it to watch.
What Operators Should Do
- Update the Ethereum app to 1.22.2 or later, on every device, today. Every model shares this code, so the device in a drawer for treasury signing is as affected as the one you use daily.
- Treat WebHID and WebUSB grants as standing permissions, not sessions. A page you connected to months ago keeps transport access in that browser profile. Revoke old grants in your browser's device settings, and keep a dedicated profile for the wallets holding real value.
- Assume vendor changelogs are not an advisory channel. Watch the bulletin feed and the release tags, and treat a release note that says only "security issues" as a signal to patch now.
- If you build a signer, fail closed at every command handler and re-check state at the sink. Copy the 1.22.2 pattern: refuse a new session while a review is pending, pin display-affecting parameters at the first chunk, and validate state again inside the approval callback before the key is used. The entry guard and the sink guard catch different bugs.
- If you hold funds behind a hardware wallet, monitor the approvals it grants. A firmware race is invisible to on-chain tooling; the allowance it produces is not. An alert on any new or raised approval from a treasury address is a cheap backstop against every signing failure.
- Scope one audit at the transport layer, not just the contract layer. Teams building wallets and signing services buy contract audits and skip the state machine deciding which bytes reach the key. This one lived entirely in that gap.
Frequently Asked Questions
Was anyone actually robbed using this? No exploitation has been reported. TestMachine reproduced it on a Ledger Flex, and both Ledger and the outlets covering the dispute describe a patched flaw with no known losses. Nobody has published telemetry either way.
Which Ledger devices and app versions are affected? The Ethereum app up to and including 1.22.1, on every model that runs it. TestMachine names Nano X, Nano S Plus, Stax, Flex and Apex as sharing the code, and the fix commit's snapshots cover Flex, Stax and Apex.
Does this mean my seed phrase or private keys were exposed? No. Nothing in the fix range touches key storage or the secure element. The key did exactly what it was asked. The bug is upstream, in what reached it.
Is there a CVE for this? Not as of August 25, 2026. An NVD keyword search returns no entries, and Ledger's bulletin series does not cover this release.
Would an audit have caught it? A review scoped to the app's APDU command handlers, yes. Every fix in the range is a state check at a function entry or a callback, visible by inspection.
Sources / References
- GitHub compare API, LedgerHQ/app-ethereum tags 1.22.1 to 1.22.2
- Commit: Reject signing commands that arrive while a review is pending
- Commit: Validate appState in signing approval callbacks
- Commit: Lock EIP-712 v1 preparatory APDUs against concurrent signing flows
- Commit: Guard address verification against concurrent signing commands
- Commit: Pin the signing mode (P2) to the value set at P1_FIRST
- Commit: Reject P2 changes on TX signing continuation chunks
- Commit: Fix appState and idle handling around reset_app_context()
- LedgerHQ/app-ethereum CHANGELOG
- Ledger Ethereum app 1.22.0 release notes
- Ledger Donjon security bulletins index
- LSB 015: TX data of unsupported crypto assets not displayed by the Ethereum app 1.6.0
- Ledger developer docs: Clear Signing overview
- ERC-7730: Structured Data Clear Signing Format
- EIP-7702: Set EOA account code
- CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition
- CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization
- TestMachine's disclosure thread
- BeInCrypto: AI firm exposes Ledger bug, CTO calls it fear-mongering after quiet fix
- AMBCrypto: Ledger pushes back on Ethereum app flaw claims
- Crypto Briefing: Ledger fixes vulnerability in Ethereum app's signing flows
- The Crypto Times: Ledger says it patched Ethereum app flaw weeks before public warning
- Chainalysis: Targeted approval phishing scams on the rise
- NVD keyword search: Ledger Ethereum app



