Anyone Could Initialise the Wallet — JavaScript Bug Hunt
Modelled on the Parity multisig wallet hack of July 2017 — the first of Parity's two 2017 incidents.
- Language: JavaScript
- Layer: Backend
- Difficulty: Medium
- Concepts: Security, Auth, State
- Modelled on: Parity multisig · July 2017
- Visible tests: an owner can withdraw from a deployed wallet; re-initialising a deployed wallet is refused
- Reward: 50 XP for a complete fix
Briefing
Modelled on the Parity multisig wallet hack of July 2017 — the first of Parity's two 2017 incidents. The wallet contracts forwarded unknown calls to a shared library, and the library's initWallet function, meant to run once at deployment, could be called again by anyone. Attackers re-initialised three wallets with themselves as the sole owner and withdrew roughly 150,000 ETH.
This project is a reconstruction in plain JavaScript: dispatch.js forwards any method name to the library (as the contract's fallback did), and wallet.js lets initWallet overwrite the owners of a wallet that is already set up.
Fix initWallet so a wallet can be initialised exactly once.
Bug report
BUG-PARITY-0719 · Priority: Critical (funds at risk) · Reported by: white-hat group
Wallet state: { owners: [], required: 0, balance, initialised }.
- initWallet(wallet, sender, owners, required) sets owners/required and marks the wallet initialised — ONLY if it is not initialised yet. A second call, from anyone (including an existing owner), throws Error("already initialised") and changes nothing.
- deploy(owners, required, balance) creates a wallet and initialises it.
- execute(wallet, sender, amount) withdraws: sender must be an owner (else throws Error("not an owner")), amount <= balance (else throws); returns the new balance.
Observed: dispatch(wallet, attacker, "initWallet", [[attacker], 1]) makes the attacker the only owner, and the next execute drains the wallet.
Logs
[chain] tx 0xeef1… initWallet([0xb3764…], 1) from 0xb3764… -> ok
[chain] tx 0x0e0f… execute(0xb3764…, 26793 ETH) from 0xb3764… -> okThe code as shipped
src/wallet/wallet.js (editable)
exports.initWallet = function (wallet, sender, owners, required) {
wallet.owners = owners.slice();
wallet.required = required;
wallet.initialised = true;
};
exports.deploy = function (owners, required, balance) {
var wallet = { owners: [], required: 0, balance: balance, initialised: false };
exports.initWallet(wallet, owners[0], owners, required);
return wallet;
};
exports.execute = function (wallet, sender, amount) {
if (wallet.owners.indexOf(sender) === -1) throw new Error("not an owner");
if (amount > wallet.balance) throw new Error("insufficient balance");
wallet.balance -= amount;
return wallet.balance;
};
Read-only context: src/wallet/dispatch.js.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.