Ledger Hacked: Pin your Dependencies
Dependency hack result in 600K theft via remote code loading
Note: Ledger has stated that have fixed the issue, and that 24 hours now the changes will be fully propagated out to all CDN caches.
Earlier this morning Ledger’s Connect Kit library was hacked by substituting the normal code with a new malicious code that would drain the user’s wallet. Over 600K was stolen, but thankfully it seems to have been frozen by Tether. In this article I will explain the basics of the code, the hack, potential fixes, and some key takeaways for better security.
To start, what is connect kit? Connect Kit is a common standard interface between a hardware wallet and the browser itself. It is defined by Wallet Connect. Ledger implemented the standard in this library to get their wallets to work in the browser, typically for dApps. By hacking their wallet connection library, it made people connecting their ledgers to any dApp at risk to lose their fund once issuing a transaction.
For the breakdown of the issue here, there are two things that happen when executing the code. The first is the library connect-kit-loader, which is a bootstrapper for the @ledgerhq/connect-kit library. The Content Kit library had not changed in the last 4 months, but earlier today multiple new versions (1.1.5, 1.1.6, 1.1.7) were published that contained code to specifically drains user’s wallets while using the wallet connection feature available in the web browser. This was not a hack of the Ledger device itself, but the JavaScript based intermediary libraries that connect your wallet to your Web Browser in order to use a dApp. A fake connector dialog would appear, high jacking the original connecter, which would then take control of the User Experience. By using this fake connector, the malicious code would be invoked when eventually signing a transaction that would drain your wallet.
So why is there a bootstrapper library for wallet connect? Often in front end applications it is easier to load remote code asynchronously, especially from CDNs that specialize in serving highly demanded code at a scalable and efficient manor, compared to your server that loads the base application. It also allows for looser coupling of dependencies in code. In this case, the connect-kit-loader was responsible for loading a remote piece of code, then executing it on the user’s machine. When loading remote code, it’s best practice to always verify what you are trying to run before running it. This is the classic “don’t curl into bash” on Linux, always verify the code you run before it could do malicious things. The error in judgement made by the team can be seen in this linked code: https://github.com/LedgerHQ/connect-kit/blob/main/packages/connect-kit-loader/src/index.ts#L83
In that linked method it does the following: creates a <script> tag with src set to the remote script location, and adds it to the DOM of the browser. The Browser’s DOM will then load the script at the specified location. The script is loaded without specifying pinned minor and patch version, meaning it will take the newest script starting with 1 in the major version number.
The malicious hacker took advantage of this and published, via stolen creds, a new version of the remote library that superseded previous versions of the code (v 1.1.4). The remote loading library would then accidently select this version as it defaults to the highest version based upon the specification (1 being the major version).
Pinning Dependencies
The first way to fix this problem is to always pin remote version of software to exact versions. Instead of only pinning the major version, make sure to specify every digit to ensure you know what you are getting. This would have been avoid if this was the chosen route.
Example fix:
const src = "https://cdn.jsdelivr.net/npm/@ledgerhq/connect-kit@1.1.4";
As to why they didn’t do this, the answer is pretty simple - it was easy.
By only pinning to the major version, this library would never have to be updated and reshipped when the remote dependency updates. In effect, it’s lazy, but a consequence of the lack of standards and maturity of Node style development. Ship fast and break things is the standard, and this line of code ensured less work for the devs of the wallet connect kit loader. Building web3 on one of the most insecure package management languages is beyond me, but that is for another time.
Integrity Checks
Another fix would be to utilize the integrity attribute on the script element, which is tailor made for these kinds of problems. You can see the DOM element created below.
If the devs added another line of code like so, they would have never had the problem to start with as the browser would prohibit the code from executing as it failed its integrity check.
script.integrity = “sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
Again this circles back to laziness, as this SHA hash would change every time the remote library changes, and the devs don’t want to constantly reship this library every time a dependency changes. A plausible solution would be to also load the SHA value remotely, from an independent server. This would create another trusted source to match increasing resiliency, though that would add more operational and infrastructure additions for the library. However, Ledger is a multi million dollar hardware wallet company, so hard criticism is deserved here.
Takeaways
This is a tough issue to find unfortunately as the remote code is loaded deep within a library that many people use and can only be found by a dev. For the average person to audit this, it would be very tough. An automated scanner that checks for dynamic <script> block creation would have been a great way to find this issue and generate a warning to use an integrity attribute and I hope that devs take advantage of such tools.
Another important key is to always have devs on your repo have maximum security on their profiles. They must use 2FA (app based, not sms), and also have verified SSH key pairs to commit and create a new package.
Additionally, there should be operational alarms that go off when a new version of a library is shipped. This would allow stakeholders of a project to realize something has gone wrong before the funds get stolen.
While there are many ways to check for dependency issues, the following are common checks to perform.
Always check the git repo of the site for any recent releases. Exercise caution when a front end rapidly or recently changes its released version. Let others be the guinea pig first and wait for steady state.
View the history of the package.json in the front end and see if there any are recent changes (use blame / annotate in the Web UI).
Ensure the frontend you are using pins their dependencies and commits their package.lock.json file, ensuring consistent builds of the software are possible, and a dependency can’t accidently be upgraded.
Ensure the front end you are using uses the HTML Integrity attribute for hash verification of its remotely loaded scripts, or does programmatic integrity checks if loaded via JavaScript. This forces the downloader to check the signature of the loaded script and will ensure there was no external tampering. Note: this only indicates tampering, if the dApp frontend is also compromised, you’ll be verifying the integrity of a malicious script, which is useless.
Always check official social media accounts and key crypto devs for any mention of compromises of your dApps. Situation normal always needs to be verified before any transaction.
Revoke privileged access for former members of a git repo.
Always audit current git repo member actions an ensure they have the least amount of privileges needed to do their work.
Have security code analyzers as part of your release cycle
Have operational alarms on deployments
Always check your hardware wallet’s transaction and compare to the browser’s transaction.