From 32f11e5b39a503488aa2ee30dd98ce3228fc0791 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Mon, 7 Sep 2026 12:41:49 +0200 Subject: [PATCH] fix: clamp {SEQ:N} width in number-format to prevent resource exhaustion formatGeneratedNumber() passed the admin-supplied {SEQ:N} width straight into String.padStart() with no upper bound. A careless or malicious format value (e.g. {SEQ:999999999}) would try to allocate a correspondingly huge string - on the backend that runs on every single honey-batch/bottling creation once such a format is saved, and in the newly added frontend preview it would run on every keystroke while typing the format. Caps the effective width at 20 digits, far beyond any realistic sequence number, in both the backend implementation and its frontend port so they stay identical. Co-Authored-By: Claude Sonnet 5 --- ApiCore-Backend/src/automation/number-format.spec.ts | 7 +++++++ ApiCore-Backend/src/automation/number-format.ts | 12 +++++++++++- ApiCore-Frontend/src/lib/automation/number-format.ts | 9 ++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/ApiCore-Backend/src/automation/number-format.spec.ts b/ApiCore-Backend/src/automation/number-format.spec.ts index 23665f3..f122531 100644 --- a/ApiCore-Backend/src/automation/number-format.spec.ts +++ b/ApiCore-Backend/src/automation/number-format.spec.ts @@ -28,4 +28,11 @@ describe('formatGeneratedNumber', () => { expect(result).toBe('FIXED-NUMBER'); }); + + it('clamps a pathologically large {SEQ:N} width instead of allocating it', () => { + const result = formatGeneratedNumber('L-{SEQ:999999999}', 7, date); + + expect(result).toBe(`L-${'0'.repeat(19)}7`); + expect(result.length).toBe(22); + }); }); diff --git a/ApiCore-Backend/src/automation/number-format.ts b/ApiCore-Backend/src/automation/number-format.ts index a916819..a041b10 100644 --- a/ApiCore-Backend/src/automation/number-format.ts +++ b/ApiCore-Backend/src/automation/number-format.ts @@ -1,3 +1,10 @@ +// Far beyond any realistic business use (sequence numbers are a handful of +// digits) but small enough that padStart can never be tricked into +// allocating a pathologically large string - {SEQ:999999999} in a +// carelessly typed format would otherwise try to build a ~1GB string on +// every single batch/bottling creation from then on. +const MAX_SEQUENCE_PAD_WIDTH = 20; + // Placeholders: {YYYY} 4-digit year, {YY} 2-digit year, {MM} month (01-12), // {DD} day (01-31), {SEQ} running sequence unpadded, {SEQ:N} sequence // zero-padded to N digits. Example matching the lastenheft's own sample @@ -18,7 +25,10 @@ export function formatGeneratedNumber( .replace(/\{MM\}/g, mm) .replace(/\{DD\}/g, dd) .replace(/\{SEQ:(\d+)\}/g, (_match, width: string) => - String(sequence).padStart(Number(width), '0'), + String(sequence).padStart( + Math.min(Number(width), MAX_SEQUENCE_PAD_WIDTH), + '0', + ), ) .replace(/\{SEQ\}/g, String(sequence)); } diff --git a/ApiCore-Frontend/src/lib/automation/number-format.ts b/ApiCore-Frontend/src/lib/automation/number-format.ts index 351da39..332c2e2 100644 --- a/ApiCore-Frontend/src/lib/automation/number-format.ts +++ b/ApiCore-Frontend/src/lib/automation/number-format.ts @@ -4,6 +4,11 @@ // Placeholders: {YYYY} 4-digit year, {YY} 2-digit year, {MM} month (01-12), // {DD} day (01-31), {SEQ} running sequence unpadded, {SEQ:N} sequence // zero-padded to N digits. +// Same cap as the backend: without it, typing {SEQ:999999999} while +// editing the format would try to build a multi-hundred-MB string on +// every keystroke and freeze the settings page. +const MAX_SEQUENCE_PAD_WIDTH = 20; + export function formatGeneratedNumber(format: string, sequence: number, date: Date): string { const yyyy = String(date.getFullYear()); const yy = yyyy.slice(-2); @@ -15,6 +20,8 @@ export function formatGeneratedNumber(format: string, sequence: number, date: Da .replace(/\{YY\}/g, yy) .replace(/\{MM\}/g, mm) .replace(/\{DD\}/g, dd) - .replace(/\{SEQ:(\d+)\}/g, (_match, width: string) => String(sequence).padStart(Number(width), "0")) + .replace(/\{SEQ:(\d+)\}/g, (_match, width: string) => + String(sequence).padStart(Math.min(Number(width), MAX_SEQUENCE_PAD_WIDTH), "0"), + ) .replace(/\{SEQ\}/g, String(sequence)); }