From feb6796377be0f364d945f50a4359ca26a733e53 Mon Sep 17 00:00:00 2001 From: eddie Date: Tue, 14 Apr 2026 13:31:56 +0800 Subject: [PATCH] Validate XRef stream /W byte widths to prevent integer overflow The /W array values from XRef streams were read without validation. A crafted PDF with large byte widths (e.g. /W [100, 100, 100]) causes excessive byte reads, integer overflow via bit shifting in parseEntries(), and NaN propagation. This adds: - Validation that each /W byte width is 0-8 per ISO 32000-1:2008, Table 17 - Bounds checking in parseEntries() for premature end of stream Fixes #1776 --- src/core/errors.ts | 9 ++++++ src/core/parser/PDFXRefStreamParser.ts | 23 +++++++++++--- tests/core/parser/PDFXRefStreamParser.spec.ts | 31 +++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/core/errors.ts b/src/core/errors.ts index f8374c482..d59534c9c 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -219,3 +219,12 @@ export class MissingKeywordError extends PDFParsingError { super(pos, msg); } } + +export class InvalidXRefStreamByteWidthError extends Error { + constructor(width: number, index: number) { + const msg = + `Invalid XRef stream /W byte width: W[${index}] = ${width}, ` + + `but must be between 0 and 8 (per ISO 32000-1:2008, Table 17)`; + super(msg); + } +} diff --git a/src/core/parser/PDFXRefStreamParser.ts b/src/core/parser/PDFXRefStreamParser.ts index e452d1152..d718e8749 100644 --- a/src/core/parser/PDFXRefStreamParser.ts +++ b/src/core/parser/PDFXRefStreamParser.ts @@ -1,4 +1,7 @@ -import { ReparseError } from 'src/core/errors'; +import { + InvalidXRefStreamByteWidthError, + ReparseError, +} from 'src/core/errors'; import PDFArray from 'src/core/objects/PDFArray'; import PDFDict from 'src/core/objects/PDFDict'; import PDFName from 'src/core/objects/PDFName'; @@ -54,7 +57,11 @@ class PDFXRefStreamParser { const W = this.dict.lookup(PDFName.of('W'), PDFArray); this.byteWidths = [-1, -1, -1]; for (let idx = 0, len = W.size(); idx < len; idx++) { - this.byteWidths[idx] = W.lookup(idx, PDFNumber).asNumber(); + const width = W.lookup(idx, PDFNumber).asNumber(); + if (width < 0 || width > 8) { + throw new InvalidXRefStreamByteWidthError(width, idx); + } + this.byteWidths[idx] = width; } } @@ -95,17 +102,23 @@ class PDFXRefStreamParser { for (let objIdx = 0; objIdx < length; objIdx++) { let type = 0; for (let idx = 0, len = typeFieldWidth; idx < len; idx++) { - type = (type << 8) | this.bytes.next(); + const byte = this.bytes.next(); + if (byte === undefined) return entries; + type = (type << 8) | byte; } let offset = 0; for (let idx = 0, len = offsetFieldWidth; idx < len; idx++) { - offset = (offset << 8) | this.bytes.next(); + const byte = this.bytes.next(); + if (byte === undefined) return entries; + offset = (offset << 8) | byte; } let generationNumber = 0; for (let idx = 0, len = genFieldWidth; idx < len; idx++) { - generationNumber = (generationNumber << 8) | this.bytes.next(); + const byte = this.bytes.next(); + if (byte === undefined) return entries; + generationNumber = (generationNumber << 8) | byte; } // When the `type` field is absent, it defaults to 1 diff --git a/tests/core/parser/PDFXRefStreamParser.spec.ts b/tests/core/parser/PDFXRefStreamParser.spec.ts index 9dd069f16..c8a202c3c 100644 --- a/tests/core/parser/PDFXRefStreamParser.spec.ts +++ b/tests/core/parser/PDFXRefStreamParser.spec.ts @@ -165,6 +165,37 @@ describe(`PDFXRefStreamParser`, () => { // expect(context.lookup(barRef)).not.toBeUndefined(); // }); + it(`throws for invalid /W byte widths exceeding 8`, () => { + const context = PDFContext.create(); + const dict = context.obj({ + Index: [0, 1], + Length: 0, + Size: 1, + W: [100, 100, 100], + }); + const contents = new Uint8Array(0); + const stream = PDFRawStream.of(dict, contents); + + expect(() => PDFXRefStreamParser.forStream(stream)).toThrow( + /Invalid XRef stream \/W byte width/, + ); + }); + + it(`accepts /W byte widths within valid range 0-8`, () => { + const context = PDFContext.create(); + const dict = context.obj({ + Index: [0, 1], + Length: 5, + Size: 1, + W: [1, 4, 0], + }); + // 1 byte type + 4 bytes offset = 5 bytes for 1 entry + const contents = new Uint8Array([1, 0, 0, 0, 1]); + const stream = PDFRawStream.of(dict, contents); + + expect(() => PDFXRefStreamParser.forStream(stream)).not.toThrow(); + }); + it(`prevents reparsing`, () => { const context = PDFContext.create(); const dict = context.obj({