fix: bound aggregate FSSHTTP references

This commit is contained in:
2026-07-22 20:10:09 +02:00
parent 16eb996ad0
commit e44ff78511
3 changed files with 113 additions and 4 deletions

View File

@@ -0,0 +1,72 @@
import { describe, expect, it } from 'vitest';
import { BinaryReader } from '../binary/BinaryReader.js';
import { resolveParserLimits } from '../parser/limits.js';
import { parseFssHttpDataElementPackage } from './data-elements.js';
import { FSSHTTP_OBJECT } from './types.js';
function start(type: number, compound: boolean, body: number[]): number[] {
const raw = (body.length << 9) | (type << 3) | (compound ? 0x04 : 0x00);
return [raw & 0xff, raw >>> 8, ...body];
}
function end(type: number): number[] {
return [(type << 2) | 0x01];
}
function objectGroupDataEntry(): number[] {
return start(FSSHTTP_OBJECT.objectGroupDataExcluded, false, [
0x03, // One object reference.
0x00, // Nil ExGUID.
0x03, // One cell reference.
0x00, // Nil context ExGUID.
0x00, // Nil object-space ExGUID.
0x00, // Excluded object data size.
]);
}
function packageWithReferenceArrays(entryCount: number): Uint8Array {
const dataEntries = Array.from({ length: entryCount }, () =>
objectGroupDataEntry()
).flat();
return Uint8Array.from([
...start(FSSHTTP_OBJECT.dataElementPackage, true, [0x00]),
...start(FSSHTTP_OBJECT.dataElement, true, [
0x00, // Nil data-element ExGUID.
0x00, // Nil serial number.
0x0b, // Object Group data-element type (5).
]),
...start(FSSHTTP_OBJECT.objectGroupDeclaration, true, []),
...end(FSSHTTP_OBJECT.objectGroupDeclaration),
...start(FSSHTTP_OBJECT.objectGroupData, true, []),
...dataEntries,
...end(FSSHTTP_OBJECT.objectGroupData),
...end(FSSHTTP_OBJECT.dataElement),
...end(FSSHTTP_OBJECT.dataElementPackage),
]);
}
describe('FSSHTTPB data elements', () => {
it('enforces one aggregate budget across many reference arrays', () => {
const bytes = packageWithReferenceArrays(16);
expect(() =>
parseFssHttpDataElementPackage(
// Every array contains one reference, but the package contains 32.
new BinaryReader(bytes),
resolveParserLimits({ maxObjectReferences: 31 })
)
).toThrowError(
expect.objectContaining({
code: 'LIMIT_EXCEEDED',
structure: 'FSSHTTPB object and cell references',
})
);
const parsed = parseFssHttpDataElementPackage(
new BinaryReader(bytes),
resolveParserLimits({ maxObjectReferences: 32 })
);
expect(parsed.objectGroups.values().next().value?.data).toHaveLength(16);
});
});

View File

@@ -27,6 +27,7 @@ import {
readFssHttpSerialNumber, readFssHttpSerialNumber,
readFssHttpStreamHeader, readFssHttpStreamHeader,
takeFssHttpHeaderBody, takeFssHttpHeaderBody,
type FssHttpReferenceBudget,
} from './primitives.js'; } from './primitives.js';
import { import {
FSSHTTP_OBJECT, FSSHTTP_OBJECT,
@@ -48,6 +49,7 @@ interface DataElementContext {
objectCount: number; objectCount: number;
blobCount: number; blobCount: number;
totalBlobBytes: number; totalBlobBytes: number;
referenceBudget: FssHttpReferenceBudget;
} }
export function parseFssHttpDataElementPackage( export function parseFssHttpDataElementPackage(
@@ -89,6 +91,7 @@ export function parseFssHttpDataElementPackage(
objectCount: 0, objectCount: 0,
blobCount: 0, blobCount: 0,
totalBlobBytes: 0, totalBlobBytes: 0,
referenceBudget: { limit: limits.maxObjectReferences, used: 0 },
}; };
while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage)) { while (!hasFssHttpEnd(reader, FSSHTTP_OBJECT.dataElementPackage)) {
@@ -533,8 +536,16 @@ function parseObjectGroupData(
const header = readFssHttpStreamHeader(reader); const header = readFssHttpStreamHeader(reader);
if (header.compound) unexpectedCompound(header, 'Object Group data entry'); if (header.compound) unexpectedCompound(header, 'Object Group data entry');
const body = takeFssHttpHeaderBody(reader, header, 'Object Group data entry'); const body = takeFssHttpHeaderBody(reader, header, 'Object Group data entry');
const objectReferences = readFssHttpExGuidArray(body, context.limits); const objectReferences = readFssHttpExGuidArray(
const cellReferences = readFssHttpCellIdArray(body, context.limits); body,
context.limits,
context.referenceBudget
);
const cellReferences = readFssHttpCellIdArray(
body,
context.limits,
context.referenceBudget
);
let result: FssHttpObjectGroupData; let result: FssHttpObjectGroupData;
if (header.type === FSSHTTP_OBJECT.objectGroupDataExcluded) { if (header.type === FSSHTTP_OBJECT.objectGroupDataExcluded) {
result = { result = {

View File

@@ -19,6 +19,11 @@ import type {
FssHttpStreamHeader, FssHttpStreamHeader,
} from './types.js'; } from './types.js';
export interface FssHttpReferenceBudget {
readonly limit: number;
used: number;
}
export function readCompactU64(reader: BinaryReader): bigint { export function readCompactU64(reader: BinaryReader): bigint {
const offset = reader.offset; const offset = reader.offset;
const first = reader.u8(); const first = reader.u8();
@@ -92,13 +97,16 @@ export function readFssHttpExGuid(reader: BinaryReader): ExGuid {
export function readFssHttpExGuidArray( export function readFssHttpExGuidArray(
reader: BinaryReader, reader: BinaryReader,
limits: OneNoteParserLimits limits: OneNoteParserLimits,
budget: FssHttpReferenceBudget
): ExGuid[] { ): ExGuid[] {
const countOffset = reader.offset;
const count = readCompactNumber( const count = readCompactNumber(
reader, reader,
limits.maxObjectReferences, limits.maxObjectReferences,
'Extended GUID array count' 'Extended GUID array count'
); );
reserveReferenceBudget(budget, count, countOffset);
const result: ExGuid[] = []; const result: ExGuid[] = [];
for (let index = 0; index < count; index += 1) { for (let index = 0; index < count; index += 1) {
result.push(readFssHttpExGuid(reader)); result.push(readFssHttpExGuid(reader));
@@ -115,13 +123,16 @@ export function readFssHttpCellId(reader: BinaryReader): FssHttpCellId {
export function readFssHttpCellIdArray( export function readFssHttpCellIdArray(
reader: BinaryReader, reader: BinaryReader,
limits: OneNoteParserLimits limits: OneNoteParserLimits,
budget: FssHttpReferenceBudget
): FssHttpCellId[] { ): FssHttpCellId[] {
const countOffset = reader.offset;
const count = readCompactNumber( const count = readCompactNumber(
reader, reader,
limits.maxObjectReferences, limits.maxObjectReferences,
'Cell ID array count' 'Cell ID array count'
); );
reserveReferenceBudget(budget, count, countOffset);
const result: FssHttpCellId[] = []; const result: FssHttpCellId[] = [];
for (let index = 0; index < count; index += 1) { for (let index = 0; index < count; index += 1) {
result.push(readFssHttpCellId(reader)); result.push(readFssHttpCellId(reader));
@@ -129,6 +140,21 @@ export function readFssHttpCellIdArray(
return result; return result;
} }
function reserveReferenceBudget(
budget: FssHttpReferenceBudget,
count: number,
offset: number
): void {
if (count > budget.limit - budget.used) {
throw new OneNoteParserError(
'LIMIT_EXCEEDED',
`FSSHTTPB object and cell reference count exceeds ${budget.limit}`,
{ offset, structure: 'FSSHTTPB object and cell references' }
);
}
budget.used += count;
}
export function readFssHttpSerialNumber( export function readFssHttpSerialNumber(
reader: BinaryReader reader: BinaryReader
): FssHttpSerialNumber { ): FssHttpSerialNumber {