From 16eb996ad0ab03231d2e1e00c5057d0f44a11696 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 22 Jul 2026 20:09:44 +0200 Subject: [PATCH] fix: bound actual table references --- src/onenote/one/content-table-safety.test.ts | 49 +++++++++++++++++++- src/onenote/one/content.ts | 13 ++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/onenote/one/content-table-safety.test.ts b/src/onenote/one/content-table-safety.test.ts index 899cf81..0824112 100644 --- a/src/onenote/one/content-table-safety.test.ts +++ b/src/onenote/one/content-table-safety.test.ts @@ -46,7 +46,7 @@ describe('content table allocation safety', () => { expect.objectContaining({ code: 'CONTENT_PARSE_FAILED', message: expect.stringContaining( - 'Declared table column count exceeds 1000000' + `Declared table column count exceeds ${DEFAULT_ONENOTE_PARSER_LIMITS.maxTableCells}` ), }) ); @@ -77,7 +77,43 @@ describe('content table allocation safety', () => { }) ).toEqual([]); expect(diagnostics[0]?.message).toContain( - 'Declared table cell count exceeds 1000000' + `Declared table cell count exceeds ${DEFAULT_ONENOTE_PARSER_LIMITS.maxTableCells}` + ); + }); + + it('rejects actual row references before mapping an oversized array', () => { + const tableId: ExGuid = { guid: GUID, value: 3 }; + const firstRowId: ExGuid = { guid: GUID, value: 4 }; + const secondRowId: ExGuid = { guid: GUID, value: 5 }; + const table = object( + tableId, + JCID.TableNode, + props(referenceEntry(PROPERTY.ElementChildNodes, 2)) + ); + table.resolvedObjectIds = [firstRowId, secondRowId]; + const firstRow = object(firstRowId, JCID.TableRowNode, props()); + const secondRow = object(secondRowId, JCID.TableRowNode, props()); + const diagnostics: ParserDiagnostic[] = []; + + expect( + parseContentObjects([tableId], { + space: { + id: { guid: GUID, value: 0 }, + roots: new Map(), + objects: new Map( + [table, firstRow, secondRow].map((value) => [ + exGuidKey(value.id), + value, + ]) + ), + }, + limits: { ...DEFAULT_ONENOTE_PARSER_LIMITS, maxTableCells: 1 }, + diagnostics, + resources: new Map(), + }) + ).toEqual([]); + expect(diagnostics[0]?.message).toContain( + 'Table row reference count exceeds 1' ); }); }); @@ -122,3 +158,12 @@ function bytesEntry(rawId: number, value: Uint8Array): PropertyEntry { value: { kind: 'bytes', value }, }; } + +function referenceEntry(rawId: number, count: number): PropertyEntry { + return { + rawId, + id: rawId & 0x03ff_ffff, + type: 0x9, + value: { kind: 'objectIds', count }, + }; +} diff --git a/src/onenote/one/content.ts b/src/onenote/one/content.ts index 63ac079..6d9bf1f 100644 --- a/src/onenote/one/content.ts +++ b/src/onenote/one/content.ts @@ -540,6 +540,11 @@ function parseTable( context.limits.maxTableCells ); const rowIds = objectReferences(object, PROPERTY.ElementChildNodes); + if (rowIds.length > context.limits.maxTableCells) { + throw new Error( + `Table row reference count exceeds ${context.limits.maxTableCells}` + ); + } if (declaredRowCount !== undefined && declaredRowCount !== rowIds.length) { addDiagnostic( context, @@ -553,6 +558,14 @@ function parseTable( const row = getObject(context.space, rowId, 'table row'); assertJcid(row, JCID.TableRowNode, 'TableRowNode'); const cellIds = objectReferences(row, PROPERTY.ElementChildNodes); + if ( + cellIds.length > + context.limits.maxTableCells - context.budget.tableCells + ) { + throw new Error( + `Table cell reference count exceeds ${context.limits.maxTableCells}` + ); + } if ( declaredColumnCount !== undefined && cellIds.length !== declaredColumnCount