From 4f8832ff45e8571501b84d01c9bb0be059ee5480 Mon Sep 17 00:00:00 2001 From: Kyle Cox Date: Thu, 8 Oct 2026 22:43:52 +0000 Subject: [PATCH] fix(serializer): throw on out-of-bounds reads in SigmaByteReader --- .changeset/tidy-reads-bound.md | 5 +++ .../src/coders/sigmaByteReader.spec.ts | 39 +++++++++++++++++++ .../serializer/src/coders/sigmaByteReader.ts | 18 +++++++++ 3 files changed, 62 insertions(+) create mode 100644 .changeset/tidy-reads-bound.md diff --git a/.changeset/tidy-reads-bound.md b/.changeset/tidy-reads-bound.md new file mode 100644 index 00000000..e3064b13 --- /dev/null +++ b/.changeset/tidy-reads-bound.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/serializer": patch +--- + +fix(serializer): throw on out-of-bounds reads in SigmaByteReader instead of returning undefined or truncated data diff --git a/packages/serializer/src/coders/sigmaByteReader.spec.ts b/packages/serializer/src/coders/sigmaByteReader.spec.ts index 79108891..1dc4ea77 100644 --- a/packages/serializer/src/coders/sigmaByteReader.spec.ts +++ b/packages/serializer/src/coders/sigmaByteReader.spec.ts @@ -62,6 +62,45 @@ describe("Sigma byte reader", () => { expect(reader.match(u8a([0x04, 0x05, 0x06, 0x07]), 3)).toBe(false); }); + it("should throw when reading a byte past the end of the input", () => { + const reader = new SigmaByteReader(u8a([0x01])); + + expect(reader.readByte()).toBe(0x01); + expect(() => reader.readByte()).toThrow(RangeError); + expect(() => new SigmaByteReader(u8a([])).readByte()).toThrow(RangeError); + }); + + it("should throw when reading more bytes than remain", () => { + const reader = new SigmaByteReader(u8a([0x01, 0x02, 0x03])); + + expect(reader.readBytes(2)).toEqual(u8a([0x01, 0x02])); + expect(() => reader.readBytes(2)).toThrow(RangeError); + expect(reader.readBytes(1)).toEqual(u8a([0x03])); // exact remainder still works + expect(() => reader.readBytes(1)).toThrow(RangeError); + expect(() => reader.readBytes(-1)).toThrow(RangeError); + }); + + it("should throw when reading bits past the end of the input", () => { + const reader = new SigmaByteReader(u8a([0xff])); + + expect(reader.readBits(8)).toEqual([true, true, true, true, true, true, true, true]); + expect(() => reader.readBits(1)).toThrow(RangeError); + expect(() => new SigmaByteReader(u8a([0xff])).readBits(9)).toThrow(RangeError); + }); + + it("should throw on a VLQ truncated with the continuation bit set", () => { + expect(() => new SigmaByteReader(u8a([0x80])).readUInt()).toThrow(RangeError); + expect(() => new SigmaByteReader(u8a([0x80])).readBigUInt()).toThrow(RangeError); + expect(() => new SigmaByteReader(u8a([0xff, 0xff])).readUInt()).toThrow(RangeError); + }); + + it("should throw when a length-prefixed field is truncated", () => { + // SLong header is not involved here: readI256 reads a VLQ length (5) and then + // expects 5 bytes, but only 2 remain. + const reader = new SigmaByteReader(u8a([0x05, 0x01, 0x02])); + expect(() => reader.readI256()).toThrow(RangeError); + }); + it("should read remaining bytes", () => { const bytes = u8a([0x01, 0x02, 0x03, 0x04]); const reader = new SigmaByteReader(bytes); diff --git a/packages/serializer/src/coders/sigmaByteReader.ts b/packages/serializer/src/coders/sigmaByteReader.ts index 6795e433..b8b0f57f 100644 --- a/packages/serializer/src/coders/sigmaByteReader.ts +++ b/packages/serializer/src/coders/sigmaByteReader.ts @@ -42,6 +42,12 @@ export class SigmaByteReader { } readBits(length: number): ArrayLike { + if (length < 0 || this.#cursor + Math.ceil(length / 8) > this.#bytes.length) { + throw new RangeError( + `Reading ${length} bits at position ${this.#cursor} exceeds the input length of ${this.#bytes.length} bytes` + ); + } + const bits = new Array(length); let bitOffset = 0; @@ -61,10 +67,22 @@ export class SigmaByteReader { } readByte(): number { + if (this.#cursor >= this.#bytes.length) { + throw new RangeError( + `Reading a byte at position ${this.#cursor} exceeds the input length of ${this.#bytes.length} bytes` + ); + } + return this.#bytes[this.#cursor++]; } readBytes(length: number): Uint8Array { + if (length < 0 || this.#cursor + length > this.#bytes.length) { + throw new RangeError( + `Reading ${length} bytes at position ${this.#cursor} exceeds the input length of ${this.#bytes.length} bytes` + ); + } + return this.#bytes.subarray(this.#cursor, (this.#cursor += length)); }