Lookup for vulnerable packages by Package URL.

Purlpkg:npm/%40lodestar/reqresp@1.19.0-dev.bda2c20f23
Typenpm
Namespace@lodestar
Namereqresp
Version1.19.0-dev.bda2c20f23
Qualifiers
Subpath
Is_vulnerabletrue
Next_non_vulnerable_version1.25.0
Latest_non_vulnerable_version1.25.0
Affected_by_vulnerabilities
0
url VCID-cwag-sjnj-43az
vulnerability_id VCID-cwag-sjnj-43az
summary
Lodestar snappy decompression issue
### Impact
Unintended permanent chain split affecting greater than or equal to 25% of the network, requiring hard fork (network partition requiring hard fork)

### Description
Lodestar client may fail to decode snappy framing compressed messages.

### Vulnerability Details
In Req/Resp protocol the message are encoded by using ssz_snappy encoding, which is basically snappy framing compression over ssz encoded message.

It's mentioned here - https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/p2p-interface.md

```
The token of the negotiated protocol ID specifies the type of encoding to be used for the req/resp interaction. Only one value is possible at this time:

ssz_snappy: The contents are first SSZ-encoded and then compressed with Snappy frames compression. For objects containing a single field, only the field is SSZ-encoded not a container with a single field. For example, the BeaconBlocksByRoot request is an SSZ-encoded list of Root's. This encoding type MUST be supported by all clients.
```

In snappy framing format there a few types of chunks.
We are interested in so called reserved skippable chunks. These are chunks with chunk type in range [0x80, 0xfd]
Let's see how rust snappy handles them https://github.com/BurntSushi/rust-snappy/blob/master/src/read.rs#L137

```
impl<R: io::Read> io::Read for FrameDecoder<R> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
 		   ... 
           ...
  		    let len = len64 as usize;
            match ty {
                Err(b) if 0x02 <= b && b <= 0x7F => {
                    // Spec says that chunk types 0x02-0x7F are reserved and
                    // conformant decoders must return an error.
                    fail!(Error::UnsupportedChunkType { byte: b });
                }
                Err(b) if 0x80 <= b && b <= 0xFD => {
                    // Spec says that chunk types 0x80-0xFD are reserved but
                    // skippable.
                    self.r.read_exact(&mut self.src[0..len])?;
                }
```

Similar code can be found in golang implementation - https://github.com/golang/snappy/blob/master/decode.go#L221

```
func (r *Reader) fill() error {
	...
	if chunkType <= 0x7f {
			// Section 4.5. Reserved unskippable chunks (chunk types 0x02-0x7f).
			r.err = ErrUnsupported
			return r.err
		}
		// Section 4.4 Padding (chunk type 0xfe).
		// Section 4.6. Reserved skippable chunks (chunk types 0x80-0xfd).
		if !r.readFull(r.buf[:chunkLen], false) {
			return r.err
		}
```

Now let's see how lodestar handles such chunks 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) {
        result.append(data.subarray(4));
      }
    }
    if (result.length === 0) {
      return null;
    }
    return result;
  }

 function getChunkType(value: number): ChunkType {
  switch (value) {
    case ChunkType.IDENTIFIER:
      return ChunkType.IDENTIFIER;
    case ChunkType.COMPRESSED:
      return ChunkType.COMPRESSED;
    case ChunkType.UNCOMPRESSED:
      return ChunkType.UNCOMPRESSED;
    case ChunkType.PADDING:
      return ChunkType.PADDING;
    default:
      throw new Error("Unsupported snappy chunk type");
  }
```

As you can see, lodestar does not recognize such chunks.

If it sees such chunk, function getChunkType() throws an exception and decoding fails.

### Impact Details

Faulty nodes may trigger chain stall by sending messages which lodestar fails to parse, while other clients will be able to handle.

### Proof of Concept

How to reproduce:

1. get archive (via provided [gist link](https://gist.github.com/gln7/bdde7f4e0bdf9d47bf810a015796867a)), decode and unpack it:
```
$ base64 -d poc.txt > poc.tgz
$ tar zxf poc.tgz
```

2. run dec1.go to verify that our snappy file decompressed successfully
```
$ go run dec1.go

reading 1.snappy...
read 124 bytes, err <nil>
```

3. run dec1.mjs to verify that lodestar fails to decode such file
```
checking chunk type=255
checking chunk type=1
got uncompressed chunk..
checking chunk type=129
file:///../poc/dec1.mjs:74
            throw new Error("Unsupported snappy chunk type");
```
references
0
reference_url https://github.com/ChainSafe/lodestar
reference_id
reference_type
scores
0
value LOW
scoring_system generic_textual
scoring_elements
url https://github.com/ChainSafe/lodestar
1
reference_url https://github.com/ChainSafe/lodestar/commit/18a0d681dbcc51fb2ac9456f31e91f4e31a18300
reference_id
reference_type
scores
0
value LOW
scoring_system generic_textual
scoring_elements
url https://github.com/ChainSafe/lodestar/commit/18a0d681dbcc51fb2ac9456f31e91f4e31a18300
2
reference_url https://github.com/ChainSafe/lodestar/security/advisories/GHSA-53rv-hcvm-rpp9
reference_id
reference_type
scores
0
value LOW
scoring_system generic_textual
scoring_elements
url https://github.com/ChainSafe/lodestar/security/advisories/GHSA-53rv-hcvm-rpp9
3
reference_url https://github.com/advisories/GHSA-53rv-hcvm-rpp9
reference_id GHSA-53rv-hcvm-rpp9
reference_type
scores
url https://github.com/advisories/GHSA-53rv-hcvm-rpp9
fixed_packages
0
url pkg:npm/%40lodestar/reqresp@1.25.0
purl pkg:npm/%40lodestar/reqresp@1.25.0
is_vulnerable false
affected_by_vulnerabilities
resource_url http://public2.vulnerablecode.io/packages/pkg:npm/%2540lodestar/reqresp@1.25.0
aliases GHSA-53rv-hcvm-rpp9
risk_score 1.4
exploitability 0.5
weighted_severity 2.7
resource_url http://public2.vulnerablecode.io/vulnerabilities/VCID-cwag-sjnj-43az
1
url VCID-jgep-67qa-cfae
vulnerability_id VCID-jgep-67qa-cfae
summary
Lodestar snappy checksum issue
### 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](https://gist.github.com/gln7/aab55674431b1c8d42a59ccf9d7cbf60) and run it:

```
$ node dec1.mjs 
checking chunk type=255
checking chunk type=1
got uncompressed chunk..
Decompressed ok 124 bytes
```
references
0
reference_url https://github.com/ChainSafe/lodestar
reference_id
reference_type
scores
0
value LOW
scoring_system generic_textual
scoring_elements
url https://github.com/ChainSafe/lodestar
1
reference_url https://github.com/ChainSafe/lodestar/commit/18a0d681dbcc51fb2ac9456f31e91f4e31a18300
reference_id
reference_type
scores
0
value LOW
scoring_system generic_textual
scoring_elements
url https://github.com/ChainSafe/lodestar/commit/18a0d681dbcc51fb2ac9456f31e91f4e31a18300
2
reference_url https://github.com/ChainSafe/lodestar/security/advisories/GHSA-m9c9-mc2h-9wjw
reference_id
reference_type
scores
0
value LOW
scoring_system generic_textual
scoring_elements
url https://github.com/ChainSafe/lodestar/security/advisories/GHSA-m9c9-mc2h-9wjw
3
reference_url https://github.com/advisories/GHSA-m9c9-mc2h-9wjw
reference_id GHSA-m9c9-mc2h-9wjw
reference_type
scores
url https://github.com/advisories/GHSA-m9c9-mc2h-9wjw
fixed_packages
0
url pkg:npm/%40lodestar/reqresp@1.25.0
purl pkg:npm/%40lodestar/reqresp@1.25.0
is_vulnerable false
affected_by_vulnerabilities
resource_url http://public2.vulnerablecode.io/packages/pkg:npm/%2540lodestar/reqresp@1.25.0
aliases GHSA-m9c9-mc2h-9wjw
risk_score 1.4
exploitability 0.5
weighted_severity 2.7
resource_url http://public2.vulnerablecode.io/vulnerabilities/VCID-jgep-67qa-cfae
Fixing_vulnerabilities
Risk_score1.4
Resource_urlhttp://public2.vulnerablecode.io/packages/pkg:npm/%2540lodestar/reqresp@1.19.0-dev.bda2c20f23