89802 w a critical missing chain identity validation in the quantus sdk signing path lets a malicious rpc endpoint obtain signatures valid on an unintended chain
Submitted on Aug 21st 2026 at 09:39:20 UTC by @bytes for Audit Comp | Quantus
Report ID: #89802
Report Type: Websites & Apps
Report severity: Critical
Target: https://github.com/immunefi-team/audit-comp-quantus-apps/tree/audit-comp-ready/quantus_sdk
Impacts:
Malicious interactions with an already-connected wallet, such as:
Modifying transaction arguments or parameters
Substituting contract addresses
Submitting malicious transactions
Description
Brief/Intro
The Quantus SDK fetches several fields of the chain's identity (genesis hash, spec version, transaction version) from the RPC endpoint that will receive the transaction and signs them without validating against any sort of local chain identity information. The SDK includes a genesis hash whitelist enforced on the cold wallet path, but the mobile signing path never calls it. A malicious RPC endpoint can choose the chain a user's signature is valid on. Since one seed derives the same address on every Quantus network, the signature can move the user's funds on a network they never selected.
Vulnerability Details
The chain ID is taken from the endpoint, unvalidated. At substrate_service.dart:365-371, _getGenesisHash will just return whatever the server returns without a comparison.
The genesis hash is signed as-is. At substrate_service.dart:214-233, the endpoint's fields go into a SigningPayload and then line 255 calls sign without inspecting the genesisHash. The same pattern repeats at 292-311.
The whitelist check exists in the same package, but is not called. At quantus_payload_parser.dart:34-38, knownNetworks defines the genesis hashes the wallet will sign for, and parsePayload enforces it at 140-143. The cold wallet calls this (sign_transaction_screen.dart:48), but substrate_service.dart has no reference to knownNetworks, parsePayload, or specMatchesBundled.
Impact Details
The genesis hash, spec version, and transaction version are parameters that are signed and therefore bind a signature to one chain. The endpoint supplies them and the wallet signs without checking any of them. The app's endpoint list is hardcoded to a single network so the correct chain is known at build time, but nothing compares against it. The confirmation screen shows the amount and recipient but never the chain so the user has no way to notice.
Funds at risk: One seed produces the same address on every Quantus network, so any user who holds a balance on more than one network can have funds moved on the unintended one. Every transaction signed through substrate_service.dart is affected. Cold wallet users are not affected.
Precondition: Control of one configured RPC endpoint. All endpoints are HTTPS, so this requires endpoint compromise rather than network interception.
References
Affected code cited inline above.
Proof of Concept
The attacker controls one of the three hardcoded RPC endpoints (quantus_sdk/lib/src/constants/app_constants.dart:15-19). All but two requests are proxied to a genuine Planck node, so the wallet behaves normally in every other respect.
The SDK accepts the attacker-controlled hash
_getGenesisHash strips off the 0x prefix and returns the attacker's value (line 371) without comparison. The wallet ships with a whitelist of the genesis hashes it will sign for (quantus_payload_parser.dart:34-38) and a function, parsePayload, that rejects any payload whose genesis hash is absent from it (lines 140-143). Neither is called anywhere in substrate_service.dart.
The same sequence of events applies to the structurally identical second signing path at lines 292-311.
Was this helpful?