Skip to content

SSZ Union Deserialization Vulnerable to Trailing Data for Certain Variants #507

@trackoor

Description

@trackoor

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:

  value_deserializeFromBytes(_data: ByteViews, _start: number): null {
    return null;
  }

PoC

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 ---

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions