Skip to content

Lodestar snappy checksum issue

Low severity GitHub Reviewed Published Jan 14, 2025 in ChainSafe/lodestar • Updated Jan 14, 2025

Package

npm @lodestar/reqresp (npm)

Affected versions

< 1.25.0

Patched versions

1.25.0

Description

Impact

Unintended permanent chain split affecting greater than or equal to 25% of the network, requiring hard fork (network partition requiring hard fork)

Lodestar does not verify checksum in snappy framing uncompressed chunks.

Vulnerability Details

In Req/Resp protocol the messages are encoded by using ssz_snappy encoding, which is a snappy framing compression over ssz encoded message.

In snappy framing format there are uncompressed chunks, each such chunk is prefixed with a checksum.

Let's see how golang implementation parses such chunks - https://github.com/golang/snappy/blob/master/decode.go#L176

	case chunkTypeUncompressedData:
			// Section 4.3. Uncompressed data (chunk type 0x01).
			if chunkLen < checksumSize {
				r.err = ErrCorrupt
				return r.err
			}
			buf := r.buf[:checksumSize]
			if !r.readFull(buf, false) {
				return r.err
			}
			checksum := uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24
			// Read directly into r.decoded instead of via r.buf.
			n := chunkLen - checksumSize
			if n > len(r.decoded) {
				r.err = ErrCorrupt
				return r.err
			}
			if !r.readFull(r.decoded[:n], false) {
				return r.err
			}
			if crc(r.decoded[:n]) != checksum {
				r.err = ErrCorrupt
				return r.err
			}
			r.i, r.j = 0, n
			continue

As you can see, if checksum is incorrect, decoder fails and returns error.

Now let's look at lodestar decoder https://github.com/ChainSafe/lodestar/blob/unstable/packages/reqresp/src/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts#L17

uncompress(chunk: Uint8ArrayList): Uint8ArrayList | null {
    this.buffer.append(chunk);
    const result = new Uint8ArrayList();
    while (this.buffer.length > 0) {
      if (this.buffer.length < 4) break;

      const type = getChunkType(this.buffer.get(0));
      const frameSize = getFrameSize(this.buffer, 1);

      if (this.buffer.length - 4 < frameSize) {
        break;
      }

      const data = this.buffer.subarray(4, 4 + frameSize);
      this.buffer.consume(4 + frameSize);

      if (!this.state.foundIdentifier && type !== ChunkType.IDENTIFIER) {
        throw "malformed input: must begin with an identifier";
      }

      if (type === ChunkType.IDENTIFIER) {
        if (!Buffer.prototype.equals.call(data, IDENTIFIER)) {
          throw "malformed input: bad identifier";
        }
        this.state.foundIdentifier = true;
        continue;
      }

      if (type === ChunkType.COMPRESSED) {
        result.append(uncompress(data.subarray(4)));
      }
      if (type === ChunkType.UNCOMPRESSED) {
1)        result.append(data.subarray(4));
      }
    }
    if (result.length === 0) {
      return null;
    }
    return result;
  }

As you can see, checksum is not verified, bytes are appended to 'result'

Proof of Concept

How to reproduce:

get poc via gist link and run it:

$ node dec1.mjs 
checking chunk type=255
checking chunk type=1
got uncompressed chunk..
Decompressed ok 124 bytes

References

@philknows philknows published to ChainSafe/lodestar Jan 14, 2025
Published to the GitHub Advisory Database Jan 14, 2025
Reviewed Jan 14, 2025
Last updated Jan 14, 2025

Severity

Low

Weaknesses

CVE ID

No known CVE

GHSA ID

GHSA-m9c9-mc2h-9wjw

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.