{"id":"GHSA-m9c9-mc2h-9wjw","aliases":[],"url":"https://o3.security/vulnerability/GHSA-m9c9-mc2h-9wjw","summary":"Lodestar snappy checksum 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\nLodestar does not verify checksum in snappy framing uncompressed chunks.\n\n### Vulnerability Details\nIn Req/Resp protocol the messages are encoded by using ssz_snappy encoding, which is a snappy framing compression over ssz encoded message.\n\nIn snappy framing format there are uncompressed chunks, each such chunk is prefixed with a checksum.\n\nLet's see how golang implementation parses such chunks - https://github.com/golang/snappy/blob/master/decode.go#L176\n\n```\n\tcase chunkTypeUncompressedData:\n\t\t\t// Section 4.3. Uncompressed data (chunk type 0x01).\n\t\t\tif chunkLen < checksumSize {\n\t\t\t\tr.err = ErrCorrupt\n\t\t\t\treturn r.err\n\t\t\t}\n\t\t\tbuf := r.buf[:checksumSize]\n\t\t\tif !r.readFull(buf, false) {\n\t\t\t\treturn r.err\n\t\t\t}\n\t\t\tchecksum := uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24\n\t\t\t// Read directly into r.decoded instead of via r.buf.\n\t\t\tn := chunkLen - checksumSize\n\t\t\tif n > len(r.decoded) {\n\t\t\t\tr.err = ErrCorrupt\n\t\t\t\treturn r.err\n\t\t\t}\n\t\t\tif !r.readFull(r.decoded[:n], false) {\n\t\t\t\treturn r.err\n\t\t\t}\n\t\t\tif crc(r.decoded[:n]) != checksum {\n\t\t\t\tr.err = ErrCorrupt\n\t\t\t\treturn r.err\n\t\t\t}\n\t\t\tr.i, r.j = 0, n\n\t\t\tcontinue\n```\n\nAs you can see, if checksum is incorrect, decoder fails and returns error.\n\nNow let's look at lodestar decoder 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) {\n1)        result.append(data.subarray(4));\n      }\n    }\n    if (result.length === 0) {\n      return null;\n    }\n    return result;\n  }\n```\n\nAs you can see, checksum is not verified, bytes are appended to 'result'\n\n### Proof of Concept\n\nHow to reproduce:\n\nget poc via [gist link](https://gist.github.com/gln7/aab55674431b1c8d42a59ccf9d7cbf60) and run it:\n\n```\n$ node dec1.mjs \nchecking chunk type=255\nchecking chunk type=1\ngot uncompressed chunk..\nDecompressed ok 124 bytes\n```\n","published":"2025-01-14T22:04:02Z","modified":"2025-01-14T22:04:02Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"@lodestar/reqresp","fixedVersion":"1.25.0"}],"fix":{"url":"https://github.com/ChainSafe/lodestar/commit/18a0d681dbcc51fb2ac9456f31e91f4e31a18300","label":"ChainSafe/lodestar@18a0d68"},"references":[{"type":"WEB","url":"https://github.com/ChainSafe/lodestar/security/advisories/GHSA-m9c9-mc2h-9wjw"},{"type":"WEB","url":"https://github.com/ChainSafe/lodestar/commit/18a0d681dbcc51fb2ac9456f31e91f4e31a18300"},{"type":"PACKAGE","url":"https://github.com/ChainSafe/lodestar"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2025-01-14T22:04:02Z"}}