GHSA-53rv-hcvm-rpp9: @lodestar/reqresp Lodestar Snappy Decompression Issue Vulnerability

LOW
CWE-703
Published
Advisory Summary

GHSA-53rv-hcvm-rpp9 is a LOW lodestar snappy decompression issue vulnerability (CWE-703: Improper Check or Handling of Exceptional Conditions) affecting @lodestar/reqresp libraries in the npm ecosystem. The issue involves lodestar snappy decompression issue. Remediation is available by upgrading affected packages to version 1.25.0.

GHSA-53rv-hcvm-rpp9 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-53rv-hcvm-rpp9
Severity LOW
CVSS v3.1 Base Score N/A
CWE Classification CWE-703: Improper Check or Handling of Exceptional Conditions
Affected Product @lodestar/reqresp
Package Ecosystem npm
Published Date
Last Updated

How to Fix GHSA-53rv-hcvm-rpp9

To remediate this vulnerability, upgrade all affected @lodestar/reqresp dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@lodestar/reqresp fixed: 1.25.0
npm install @lodestar/reqresp@1.25.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @lodestar/reqresp

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-53rv-hcvm-rpp9, inspect your project dependencies using npm:

npm ls @lodestar/reqresp

Analyze the terminal output to locate any version of @lodestar/reqresp in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.25.0), your application is affected.

Note: The npm ls @lodestar/reqresp command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-703: Improper Check or Handling of Exceptional Conditions, where the product does not properly anticipate or handle exceptional conditions that rarely occur during normal operation of the product. In @lodestar/reqresp, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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"); ```

GHSA-53rv-hcvm-rpp9 Affected @lodestar/reqresp Versions

1 component(s)
Fixed Versions:
1.25.0
Product / Package Ecosystem Affected Versions Fixed Version
@lodestar/reqresp
npm
< 1.25.0
1.25.0

References

Frequently Asked Questions

What is GHSA-53rv-hcvm-rpp9?
GHSA-53rv-hcvm-rpp9 is a security vulnerability in @lodestar/reqresp involving Lodestar snappy decompression issue under CWE-703: Improper Check or Handling of Exceptional Conditions.
Which @lodestar/reqresp versions are affected?
Affected versions include releases prior to 1.25.0 across affected packages: @lodestar/reqresp.
What version fixes GHSA-53rv-hcvm-rpp9?
Upgrading to 1.25.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @lodestar/reqresp affected?
Yes, @lodestar/reqresp is affected in releases prior to 1.25.0. Upgrade your dependency to 1.25.0 or later.
What CWE is associated with GHSA-53rv-hcvm-rpp9?
GHSA-53rv-hcvm-rpp9 is classified under CWE-703: Improper Check or Handling of Exceptional Conditions (The product does not properly anticipate or handle exceptional conditions that rarely occur during normal operation of the product.).
How can I check npm dependencies for GHSA-53rv-hcvm-rpp9?
Run the dependency tree command: 'npm ls @lodestar/reqresp'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-53rv-hcvm-rpp9",
  "modified" : "2025-01-14T22:03:59Z",
  "published" : "2025-01-14T22:03:59Z",
  "aliases" : [ ],
  "summary" : "Lodestar snappy decompression issue",
  "details" : "### Impact\nUnintended permanent chain split affecting greater than or equal to 25% of the network, requiring hard fork (network partition requiring hard fork)\n\n### Description\nLodestar client may fail to decode snappy framing compressed messages.\n\n### Vulnerability Details\nIn Req/Resp protocol the message are encoded by using ssz_snappy encoding, which is basically snappy framing compression over ssz encoded message.\n\nIt's mentioned here - https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/p2p-interface.md\n\n```\nThe 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:\n\nssz_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.\n```\n\nIn snappy framing format there a few types of chunks.\nWe are interested in so called reserved skippable chunks. These are chunks with chunk type in range [0x80, 0xfd]\nLet's see how rust snappy handles them https://github.com/BurntSushi/rust-snappy/blob/master/src/read.rs#L137\n\n```\nimpl<R: io::Read> io::Read for FrameDecoder<R> {\n    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {\n \t\t   ... \n           ...\n  \t\t    let len = len64 as usize;\n            match ty {\n                Err(b) if 0x02 <= b && b <= 0x7F => {\n                    // Spec says that chunk types 0x02-0x7F are reserved and\n                    // conformant decoders must return an error.\n                    fail!(Error::UnsupportedChunkType { byte: b });\n                }\n                Err(b) if 0x80 <= b && b <= 0xFD => {\n                    // Spec says that chunk types 0x80-0xFD are reserved but\n                    // skippable.\n                    self.r.read_exact(&mut self.src[0..len])?;\n                }\n```\n\nSimilar code can be found in golang implementation - https://github.com/golang/snappy/blob/master/decode.go#L221\n\n```\nfunc (r *Reader) fill() error {\n\t...\n\tif chunkType <= 0x7f {\n\t\t\t// Section 4.5. Reserved unskippable chunks (chunk types 0x02-0x7f).\n\t\t\tr.err = ErrUnsupported\n\t\t\treturn r.err\n\t\t}\n\t\t// Section 4.4 Padding (chunk type 0xfe).\n\t\t// Section 4.6. Reserved skippable chunks (chunk types 0x80-0xfd).\n\t\tif !r.readFull(r.buf[:chunkLen], false) {\n\t\t\treturn r.err\n\t\t}\n```\n\nNow let's see how lodestar handles such chunks https://github.com/ChainSafe/lodestar/blob/unstable/packages/reqresp/src/encodingStrategies/sszSnappy/snappyFrames/uncompress.ts#L17\n\n```\nuncompress(chunk: Uint8ArrayList): Uint8ArrayList | null {\n    this.buffer.append(chunk);\n    const result = new Uint8ArrayList();\n    while (this.buffer.length > 0) {\n      if (this.buffer.length < 4) break;\n\n      const type = getChunkType(this.buffer.get(0));\n      const frameSize = getFrameSize(this.buffer, 1);\n\n      if (this.buffer.length - 4 < frameSize) {\n        break;\n      }\n\n      const data = this.buffer.subarray(4, 4 + frameSize);\n      this.buffer.consume(4 + frameSize);\n\n      if (!this.state.foundIdentifier && type !== ChunkType.IDENTIFIER) {\n        throw \"malformed input: must begin with an identifier\";\n      }\n\n      if (type === ChunkType.IDENTIFIER) {\n        if (!Buffer.prototype.equals.call(data, IDENTIFIER)) {\n          throw \"malformed input: bad identifier\";\n        }\n        this.state.foundIdentifier = true;\n        continue;\n      }\n\n      if (type === ChunkType.COMPRESSED) {\n        result.append(uncompress(data.subarray(4)));\n      }\n      if (type === ChunkType.UNCOMPRESSED) {\n        result.append(data.subarray(4));\n      }\n    }\n    if (result.length === 0) {\n      return null;\n    }\n    return result;\n  }\n\n function getChunkType(value: number): ChunkType {\n  switch (value) {\n    case ChunkType.IDENTIFIER:\n      return ChunkType.IDENTIFIER;\n    case ChunkType.COMPRESSED:\n      return ChunkType.COMPRESSED;\n    case ChunkType.UNCOMPRESSED:\n      return ChunkType.UNCOMPRESSED;\n    case ChunkType.PADDING:\n      return ChunkType.PADDING;\n    default:\n      throw new Error(\"Unsupported snappy chunk type\");\n  }\n```\n\nAs you can see, lodestar does not recognize such chunks.\n\nIf it sees such chunk, function getChunkType() throws an exception and decoding fails.\n\n### Impact Details\n\nFaulty nodes may trigger chain stall by sending messages which lodestar fails to parse, while other clients will be able to handle.\n\n### Proof of Concept\n\nHow to reproduce:\n\n1. get archive (via provided [gist link](https://gist.github.com/gln7/bdde7f4e0bdf9d47bf810a015796867a)), decode and unpack it:\n```\n$ base64 -d poc.txt > poc.tgz\n$ tar zxf poc.tgz\n```\n\n2. run dec1.go to verify that our snappy file decompressed successfully\n```\n$ go run dec1.go\n\nreading 1.snappy...\nread 124 bytes, err <nil>\n```\n\n3. run dec1.mjs to verify that lodestar fails to decode such file\n```\nchecking chunk type=255\nchecking chunk type=1\ngot uncompressed chunk..\nchecking chunk type=129\nfile:///../poc/dec1.mjs:74\n            throw new Error(\"Unsupported snappy chunk type\");\n```\n",
  "severity" : [ ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@lodestar/reqresp"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.25.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/ChainSafe/lodestar/security/advisories/GHSA-53rv-hcvm-rpp9"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/ChainSafe/lodestar/commit/18a0d681dbcc51fb2ac9456f31e91f4e31a18300"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/ChainSafe/lodestar"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-703" ],
    "severity" : "LOW",
    "github_reviewed" : true,
    "github_reviewed_at" : "2025-01-14T22:03:59Z",
    "nvd_published_at" : null
  }
}