Summary
An XCM issue was reported via the bug bounty program. The issue allowed any account to act as any chain via XCM. In Asset Hub it means root access for many chains. In general it gives them access to the sovereign accounts. This was not exploited. The issue was patched on all system parachains, Polkadot and Kusama and communications established with all ecosystem teams who also patched. We’re now working on making sure these issues are found and fixed earlier.
The bug
A reporter claimed the InitiateTransfer instruction in the XCM executor contains a logic flaw where ClearOrigin followed by InitiateTransfer { preserve_origin: true } produces an outbound message with no origin-modifying instruction (neither AliasOrigin nor ClearOrigin). This causes the destination chain to process user-supplied remote_xcm with the HRMP/UMP transport sender’s origin (e.g., Parachain(1000) for Asset Hub). Combined with the relay chain’s LocationAsSuperuser configuration for Asset Hub, this allows any signed account on Asset Hub to dispatch arbitrary calls as root on the Polkadot relay chain.
It also allows an account on any chain to impersonate it and control the sovereign accounts of that chain on other chains. For example, if a user on Parachain(2000) were to exploit this and send an XCM to Asset Hub, it could impersonate Parachain(2000) and take all the funds in its sovereign account.
Affected Repository
polkadot-sdk (XCM executor: polkadot/xcm/xcm-executor/src/lib.rs)
Details about the bug
The bug is at lines 1369-1384 of the XCM executor:
When preserve_origin = true and self.origin_ref() returns None (because ClearOrigin was executed earlier), the if let Some(…) fails silently. The code pushes nothing – no AliasOrigin, no ClearOrigin. The else branch (which pushes ClearOrigin) is unreachable because the outer if preserve_origin is true.
This is a regression introduced by PR#7423, which was the fix for a security issue (fee bypass via UnpaidExecution ordering). Before PR #7423, preserve_origin: true with origin = None would return XcmError::BadOrigin. The refactored code silently skips instead.
Timeline
All times are in UTC
-
Monday 16 February - 15:54: Security team triages a report submitted to Parity’s Bug Bounty.
-
Monday 16 February - 15:59: Security confirms and validates the impact of the report, Engineers are informed of the report.
-
Monday 16 February - 16:11: Incident room channel in element created
-
Monday 16 February - 16:49: Mitigation with XcmExecuteFilter thought of
-
Monday 16 February - 17:02: Mitigation wouldn’t work for send
-
Monday 16 February - 17:48: Fix needs to go on unstable2507
-
Monday 16 February - 18:40: Fix needed changes to pass existing tests
-
Monday 16 February - 19:11: staging-xcm-executor patch version 23.0.1 published
-
Monday 16 February - 19:24: running PET against release 2.0.7 to make sure the fix introduced no regressions
-
Monday 16 February - 20:25: Polkadot OpenGov Proposal up
-
Monday 16 February - 20:27: Fellowship Whitelist Proposal up
-
Monday 16 February - 20:30: Fellowship Whitelist Proposal decision deposit placed
-
Monday 16 February - 20:52: OpenGov Proposal decision deposit placed
-
Monday 16 February - 20:55: OpenGov Proposal decision started
-
Monday 16 February - 20:57: Fellowship Whitelist Proposal decision started
-
Monday 16 February - 22:14: Created list of affected non-system parachains for Security to reach out and help them patch.
-
Monday 16 February - 23:25: First batch of non-system parachains contacted by security to disclose the vulnerability details and offer version backports.
-
Tuesday 17 February - 13:46: stable2512 patched
-
staging-xcm-executor 24.0.1
-
polkadot-sdk 2512.3.2
-
-
Tuesday 17 February - 13:57: stable2503 patched
- staging-xcm-executor 19.1.4
-
Tuesday 17 February - 16:55: Parachain patched
-
Tuesday 17 February - 21:41: Security confirms with the Data team there are no previous traces of exploitation.
-
Wednesday 18 February - 14:44: Kusama OpenGov Proposal submitted
-
Wednesday 18 February - 14:44: Kusama Fellowship Whitelist Proposal submitted
-
Wednesday 18 February - 14:48: Kusama Fellowship Whitelist Proposal decision deposit placed
-
Wednesday 18 February - 15:54: stable2506 and stable2509 patched
-
staging-xcm-executor 20.0.2
-
staging-xcm-executor 22.0.1
-
-
Wednesday 18 February - 16:37: Fellowship Whitelist Proposal confirmation started
-
Wednesday 18 February - 17:08: Fellowship Whitelist Proposal confirmed
-
Wednesday 18 February - 17:13: Fellowship Whitelist Proposal executed
-
Wednesday 18 February - 16:36: Kusama OpenGov Proposal decision deposit placed
-
Wednesday 18 February - 18:42: Polkadot OpenGov Proposal confirmation started
-
Wednesday 18 February - 18:52: Polkadot OpenGov Proposal confirmed
-
Wednesday 18 February - 19:02: Polkadot OpenGov Proposal executed
-
Wednesday 18 February - 20:06: Polkadot system parachains patched
-
Wednesday 18 February - 21:58: Parachain patched.
-
Thursday 19 February - 18:32: Parity Security and Data teams set up transaction monitoring across the ecosystem to detect traces of exploitation.
-
Friday 20 February - 16:26: Parachain patched.
-
Saturday 21 February - 21:44: Parachain patched.
-
Monday 23 February - 06:38: Kusama Fellowship Whitelist Proposal executed.
-
Monday 23 February - 10:44: Kusama OpenGov Proposal executed.
-
Tuesday 24 February - 16:54: Parachain patched.
-
Friday 27 February - 21:24: Parachain patched.
RCA (Root Cause Analysis)
The root cause was pointed out by the bug bounty reporter.
-
Why was this vulnerability not caught before the release of the code?
- The original code was the introduction of XCMv5. That was reviewed and audited. The PR that introduced this vulnerability was a fix for an issue that arose during the audit. This PR was also reviewed as is customary. The process was followed as it should have been. However, the vulnerability was still not caught.
-
Why didn’t we find it earlier? After the review?
- It was in an unhappy path we don’t expect any honest user to use. No system chain usage stumbled upon it, or community chain. This bug also only existed in XCMv5 and the transition to this new version took a long time on the majority of chains.
-
Why wasn’t it caught by a fuzzer?
- The generated XCM can’t do anything on its own, it only results in malicious activity when sent to another chain.
-
Why couldn’t we catch this earlier than the Bounty Hunter, as the vulnerability was present for nearly a year?
- We do actively search for security issues but the codebase is too big.
-
How did the Bounty Hunter catch it? Anything we can learn from that, or a tool we can use?
- The reporter is planning to write a post detailing the process, we will review it after it’s out
Extra
-
Why did it take ~21hs for the Fellowship Whitelist to execute?
- There are many inactive fellows that while they don’t vote they raise the support threshold needed for proposals to pass faster
Action Items
-
Improve the XCM fuzzer, include more chains and more invariants to check for.
-
Deploy pause-tx or safe-mode pallets to system parachains (issue: https://github.com/polkadot-fellows/runtimes/issues/1089)
-
Maintain a list of invariants (LLMs can really help build it) and check against it in CI for each PR (also automatable through AIs)
- Can be per-topic / per code area / per components (consensus, weights, runtime state, XCM, etc)
-
Do a first screening of all PRs against potential security vulnerabilities with LLMs
-
Kick a bunch of inactive fellows to decrease the whitelisting proposal duration by up to 25 hrs (in case of 2.0.7).
Acknowledgement
This vulnerability report was submitted and rewarded to Gianluca Brigandi (Perimeter) who wanted to be publicly acknowledged.