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 <[email protected]>
This commit is contained in:
1 parent
1c16db114e
commit
32f11e5b39
3 files changed
+26
-2
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user