fix: bound aggregate nested property sets

This commit is contained in:
2026-07-22 20:15:46 +02:00
parent 6a87ef6510
commit edb19bfcc8
2 changed files with 92 additions and 7 deletions

View File

@@ -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

View File

@@ -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,
];
}