From edb19bfcc884febdb6ee5523e12824eded9c180e Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 22 Jul 2026 20:15:46 +0200 Subject: [PATCH] fix: bound aggregate nested property sets --- src/onenote/onestore/property-set.ts | 40 +++++++++++++++---- tests/binary-property.test.ts | 59 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/onenote/onestore/property-set.ts b/src/onenote/onestore/property-set.ts index c83698b..cb1e787 100644 --- a/src/onenote/onestore/property-set.ts +++ b/src/onenote/onestore/property-set.ts @@ -27,6 +27,11 @@ interface ObjectStreamHeader { osidStreamNotPresent: boolean; } +interface PropertySetParseContext { + limits: OneNoteParserLimits; + nestedSetCount: number; +} + export function parseObjectPropSet( bytes: Uint8Array, reference: ChunkReference, @@ -77,7 +82,7 @@ function parseObjectPropSetReader( } } - const properties = readPropertySet(reader, limits, 0); + const properties = readPropertySet(reader, { limits, nestedSetCount: 0 }, 0); return { objectIds, objectSpaceIds, contextIds, properties }; } @@ -180,9 +185,10 @@ function readCompactIds(reader: BinaryReader, count: number): CompactId[] { function readPropertySet( reader: BinaryReader, - limits: OneNoteParserLimits, + context: PropertySetParseContext, depth: number ): PropertySet { + const { limits } = context; if (depth > limits.maxPropertyDepth) { throw new OneNoteParserError( 'LIMIT_EXCEEDED', @@ -209,7 +215,7 @@ function readPropertySet( rawId, id: rawId & 0x03ffffff, type, - value: readPropertyValue(reader, rawId, type, limits, depth), + value: readPropertyValue(reader, rawId, type, context, depth), } satisfies PropertyEntry; }); return { entries }; @@ -219,9 +225,10 @@ function readPropertyValue( reader: BinaryReader, rawId: number, type: number, - limits: OneNoteParserLimits, + context: PropertySetParseContext, depth: number ): PropertyValue { + const { limits } = context; switch (type) { case 0x1: return { kind: 'empty' }; @@ -262,19 +269,23 @@ function readPropertyValue( case 0xd: return { kind: 'contextIds', count: boundedCount(reader, limits) }; case 0x10: { + const countOffset = reader.offset; const count = boundedCount(reader, limits); + reserveNestedPropertySets(context, count, countOffset); const id = reader.u32(); const sets: PropertySet[] = []; for (let index = 0; index < count; index += 1) { - sets.push(readPropertySet(reader, limits, depth + 1)); + sets.push(readPropertySet(reader, context, depth + 1)); } return { kind: 'propertyValues', id, sets }; } - case 0x11: + case 0x11: { + reserveNestedPropertySets(context, 1, reader.offset); return { kind: 'propertySet', - set: readPropertySet(reader, limits, depth + 1), + set: readPropertySet(reader, context, depth + 1), }; + } default: throw new OneNoteParserError( 'INVALID_STRUCTURE', @@ -284,6 +295,21 @@ function readPropertyValue( } } +function reserveNestedPropertySets( + context: PropertySetParseContext, + count: number, + offset: number +): void { + if (count > context.limits.maxObjectReferences - context.nestedSetCount) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Nested PropertySet count exceeds ${context.limits.maxObjectReferences}`, + { offset, structure: 'Nested PropertySets' } + ); + } + context.nestedSetCount += count; +} + function boundedCount( reader: BinaryReader, limits: OneNoteParserLimits diff --git a/tests/binary-property.test.ts b/tests/binary-property.test.ts index 48e1b9f..3443dde 100644 --- a/tests/binary-property.test.ts +++ b/tests/binary-property.test.ts @@ -71,4 +71,63 @@ describe('bounded OneStore primitives', () => { value: Uint8Array.from([0xaa, 0xbb, 0xcc]), }); }); + + it('enforces one aggregate budget across nested PropertySet arrays', () => { + const bytes = objectPropertySetWithNestedArrays(16); + const reference = { + offset: 0, + size: bytes.length, + nil: false, + zero: false, + }; + + expect(() => + parseObjectPropSet( + bytes, + reference, + // Every array contains one set, but the property set contains 16. + resolveParserLimits({ maxObjectReferences: 15 }) + ) + ).toThrowError( + expect.objectContaining({ + code: 'LIMIT_EXCEEDED', + structure: 'Nested PropertySets', + }) + ); + + const parsed = parseObjectPropSet( + bytes, + reference, + resolveParserLimits({ maxObjectReferences: 16 }) + ); + expect(parsed.properties.entries).toHaveLength(16); + }); }); + +function objectPropertySetWithNestedArrays(count: number): Uint8Array { + const propertyIds = Array.from({ length: count }, (_, index) => + u32((0x10 << 26) | index) + ).flat(); + const values = Array.from({ length: count }, (_, index) => [ + ...u32(1), // One nested PropertySet in this array. + ...u32(index), + 0x00, + 0x00, // Empty nested PropertySet. + ]).flat(); + return Uint8Array.from([ + ...u32(0x8000_0000), // Zero OIDs; OSID stream absent. + count & 0xff, + count >>> 8, + ...propertyIds, + ...values, + ]); +} + +function u32(value: number): number[] { + return [ + value & 0xff, + (value >>> 8) & 0xff, + (value >>> 16) & 0xff, + value >>> 24, + ]; +}