fix: bound aggregate nested property sets
This commit is contained in:
@@ -27,6 +27,11 @@ interface ObjectStreamHeader {
|
|||||||
osidStreamNotPresent: boolean;
|
osidStreamNotPresent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PropertySetParseContext {
|
||||||
|
limits: OneNoteParserLimits;
|
||||||
|
nestedSetCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseObjectPropSet(
|
export function parseObjectPropSet(
|
||||||
bytes: Uint8Array,
|
bytes: Uint8Array,
|
||||||
reference: ChunkReference,
|
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 };
|
return { objectIds, objectSpaceIds, contextIds, properties };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,9 +185,10 @@ function readCompactIds(reader: BinaryReader, count: number): CompactId[] {
|
|||||||
|
|
||||||
function readPropertySet(
|
function readPropertySet(
|
||||||
reader: BinaryReader,
|
reader: BinaryReader,
|
||||||
limits: OneNoteParserLimits,
|
context: PropertySetParseContext,
|
||||||
depth: number
|
depth: number
|
||||||
): PropertySet {
|
): PropertySet {
|
||||||
|
const { limits } = context;
|
||||||
if (depth > limits.maxPropertyDepth) {
|
if (depth > limits.maxPropertyDepth) {
|
||||||
throw new OneNoteParserError(
|
throw new OneNoteParserError(
|
||||||
'LIMIT_EXCEEDED',
|
'LIMIT_EXCEEDED',
|
||||||
@@ -209,7 +215,7 @@ function readPropertySet(
|
|||||||
rawId,
|
rawId,
|
||||||
id: rawId & 0x03ffffff,
|
id: rawId & 0x03ffffff,
|
||||||
type,
|
type,
|
||||||
value: readPropertyValue(reader, rawId, type, limits, depth),
|
value: readPropertyValue(reader, rawId, type, context, depth),
|
||||||
} satisfies PropertyEntry;
|
} satisfies PropertyEntry;
|
||||||
});
|
});
|
||||||
return { entries };
|
return { entries };
|
||||||
@@ -219,9 +225,10 @@ function readPropertyValue(
|
|||||||
reader: BinaryReader,
|
reader: BinaryReader,
|
||||||
rawId: number,
|
rawId: number,
|
||||||
type: number,
|
type: number,
|
||||||
limits: OneNoteParserLimits,
|
context: PropertySetParseContext,
|
||||||
depth: number
|
depth: number
|
||||||
): PropertyValue {
|
): PropertyValue {
|
||||||
|
const { limits } = context;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 0x1:
|
case 0x1:
|
||||||
return { kind: 'empty' };
|
return { kind: 'empty' };
|
||||||
@@ -262,19 +269,23 @@ function readPropertyValue(
|
|||||||
case 0xd:
|
case 0xd:
|
||||||
return { kind: 'contextIds', count: boundedCount(reader, limits) };
|
return { kind: 'contextIds', count: boundedCount(reader, limits) };
|
||||||
case 0x10: {
|
case 0x10: {
|
||||||
|
const countOffset = reader.offset;
|
||||||
const count = boundedCount(reader, limits);
|
const count = boundedCount(reader, limits);
|
||||||
|
reserveNestedPropertySets(context, count, countOffset);
|
||||||
const id = reader.u32();
|
const id = reader.u32();
|
||||||
const sets: PropertySet[] = [];
|
const sets: PropertySet[] = [];
|
||||||
for (let index = 0; index < count; index += 1) {
|
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 };
|
return { kind: 'propertyValues', id, sets };
|
||||||
}
|
}
|
||||||
case 0x11:
|
case 0x11: {
|
||||||
|
reserveNestedPropertySets(context, 1, reader.offset);
|
||||||
return {
|
return {
|
||||||
kind: 'propertySet',
|
kind: 'propertySet',
|
||||||
set: readPropertySet(reader, limits, depth + 1),
|
set: readPropertySet(reader, context, depth + 1),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new OneNoteParserError(
|
throw new OneNoteParserError(
|
||||||
'INVALID_STRUCTURE',
|
'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(
|
function boundedCount(
|
||||||
reader: BinaryReader,
|
reader: BinaryReader,
|
||||||
limits: OneNoteParserLimits
|
limits: OneNoteParserLimits
|
||||||
|
|||||||
@@ -71,4 +71,63 @@ describe('bounded OneStore primitives', () => {
|
|||||||
value: Uint8Array.from([0xaa, 0xbb, 0xcc]),
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user