diff --git a/packages/common/src/i18n/format_number.ts b/packages/common/src/i18n/format_number.ts index 252593e38815..6c7aa1118055 100644 --- a/packages/common/src/i18n/format_number.ts +++ b/packages/common/src/i18n/format_number.ts @@ -8,6 +8,7 @@ import {ɵRuntimeError as RuntimeError} from '@angular/core'; +import {RuntimeErrorCode} from '../errors'; import { getLocaleNumberFormat, getLocaleNumberSymbol, @@ -15,7 +16,6 @@ import { NumberFormatStyle, NumberSymbol, } from './locale_data_api'; -import {RuntimeErrorCode} from '../errors'; export const NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/; const MAX_DIGITS = 22; @@ -77,6 +77,20 @@ function formatNumberToLocaleString( } else if (minFractionPart != null && minFraction > maxFraction) { maxFraction = minFraction; } + + // Prevent DoS via resource exhaustion by capping the maximum padding iterations + const MAX_ALLOWED_DIGITS = 100; + if ( + minInt > MAX_ALLOWED_DIGITS || + minFraction > MAX_ALLOWED_DIGITS || + maxFraction > MAX_ALLOWED_DIGITS + ) { + throw new RuntimeError( + RuntimeErrorCode.INVALID_DIGIT_INFO, + ngDevMode && + `${digitsInfo} is not a valid digit info. Exceeded maximum limits of ${MAX_ALLOWED_DIGITS} digits.`, + ); + } } roundNumber(parsedNumber, minFraction, maxFraction); diff --git a/packages/common/test/i18n/format_number_spec.ts b/packages/common/test/i18n/format_number_spec.ts index 410f7287b4a9..0f42e955c695 100644 --- a/packages/common/test/i18n/format_number_spec.ts +++ b/packages/common/test/i18n/format_number_spec.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ +import {ɵDEFAULT_LOCALE_ID, ɵregisterLocaleData, ɵunregisterLocaleData} from '@angular/core'; import {formatCurrency, formatNumber, formatPercent} from '../../index'; import localeAr from '../../locales/ar'; import localeEn from '../../locales/en'; import localeEsUS from '../../locales/es-US'; import localeFr from '../../locales/fr'; -import {ɵDEFAULT_LOCALE_ID, ɵregisterLocaleData, ɵunregisterLocaleData} from '@angular/core'; describe('Format number', () => { beforeAll(() => { @@ -43,6 +43,18 @@ describe('Format number', () => { /is higher than the maximum/, ); }); + + it('should throw if minInt, minFraction, or maxFraction exceeds 100 to prevent DoS', () => { + const expectedError = /Exceeded maximum limits of 100 digits/; + expect(() => formatNumber(1.1, ɵDEFAULT_LOCALE_ID, '101.4-5')).toThrowError(expectedError); + expect(() => formatNumber(1.1, ɵDEFAULT_LOCALE_ID, '3.101-105')).toThrowError( + expectedError, + ); + expect(() => formatNumber(1.1, ɵDEFAULT_LOCALE_ID, '3.4-101')).toThrowError(expectedError); + expect(() => formatNumber(1.1, ɵDEFAULT_LOCALE_ID, '1.2000000000-20000000')).toThrowError( + expectedError, + ); + }); }); describe('transform with custom locales', () => {