From ca761eac5f2914f6b77737b50e678324bfa8d959 Mon Sep 17 00:00:00 2001 From: easedu Date: Sat, 7 Mar 2026 18:24:49 -0300 Subject: [PATCH 1/3] fix(isBase64): replace regex with iterative validation to prevent stack overflow V8's regex engine uses internal recursion proportional to string length, causing 'Maximum call stack size exceeded' on large inputs. Replace all 4 regex patterns with iterative character-code validation that runs in O(n) time with O(1) stack usage. Closes #2573 --- src/lib/isBase64.js | 41 +++++++++++++++++++++++++------- test/validators/isBase64.test.js | 20 ++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/lib/isBase64.js b/src/lib/isBase64.js index fd876e4c0..95fc5960e 100644 --- a/src/lib/isBase64.js +++ b/src/lib/isBase64.js @@ -1,10 +1,18 @@ import assertString from './util/assertString'; import merge from './util/merge'; -const base64WithPadding = /^[A-Za-z0-9+/]+={0,2}$/; -const base64WithoutPadding = /^[A-Za-z0-9+/]+$/; -const base64UrlWithPadding = /^[A-Za-z0-9_-]+={0,2}$/; -const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/; +function isValidBase64Char(code, urlSafe) { + // A-Z (65-90), a-z (97-122), 0-9 (48-57) + if ((code >= 65 && code <= 90) + || (code >= 97 && code <= 122) + || (code >= 48 && code <= 57)) { + return true; + } + if (urlSafe) { + return code === 45 || code === 95; // - _ + } + return code === 43 || code === 47; // + / +} export default function isBase64(str, options) { assertString(str); @@ -14,12 +22,27 @@ export default function isBase64(str, options) { if (options.padding && str.length % 4 !== 0) return false; - let regex; - if (options.urlSafe) { - regex = options.padding ? base64UrlWithPadding : base64UrlWithoutPadding; + if (options.padding) { + // Count trailing '=' padding + let paddingCount = 0; + let len = str.length; + while (paddingCount < len && str.charCodeAt(len - 1 - paddingCount) === 61) { + paddingCount += 1; + } + if (paddingCount > 2) return false; + + const dataLen = len - paddingCount; + if (dataLen === 0) return false; + + for (let i = 0; i < dataLen; i++) { + if (!isValidBase64Char(str.charCodeAt(i), options.urlSafe)) return false; + } } else { - regex = options.padding ? base64WithPadding : base64WithoutPadding; + // No padding allowed — all chars must be valid base64 + for (let i = 0; i < str.length; i++) { + if (!isValidBase64Char(str.charCodeAt(i), options.urlSafe)) return false; + } } - return (!options.padding || str.length % 4 === 0) && regex.test(str); + return true; } diff --git a/test/validators/isBase64.test.js b/test/validators/isBase64.test.js index c0074343a..195c6a8eb 100644 --- a/test/validators/isBase64.test.js +++ b/test/validators/isBase64.test.js @@ -198,4 +198,24 @@ describe('isBase64', () => { ], }); }); + + it('should not cause stack overflow on large strings', () => { + // Valid base64 ~1MB + const largeValid = Buffer.alloc(1000000).toString('base64'); + if (!validator.isBase64(largeValid)) { + throw new Error('isBase64() failed for a large valid base64 string'); + } + + // Invalid: large base64 with an invalid character in the middle + const largeInvalid = `${largeValid.slice(0, 500000)}!${largeValid.slice(500001)}`; + if (validator.isBase64(largeInvalid)) { + throw new Error('isBase64() should have failed for a large invalid base64 string'); + } + + // URL-safe variant + const largeUrlSafe = largeValid.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + if (!validator.isBase64(largeUrlSafe, { urlSafe: true })) { + throw new Error('isBase64() failed for a large valid base64url string'); + } + }); }); From 309fd1beb9878d62a2b460c651a693477ff63e27 Mon Sep 17 00:00:00 2001 From: easedu Date: Sat, 7 Mar 2026 19:25:45 -0300 Subject: [PATCH 2/3] refactor(isBase64): remove unreachable dead branch The dataLen === 0 check was unreachable: length % 4 guard and paddingCount > 2 guard already prevent this condition. Removing resolves the partial coverage flag from Codecov. --- src/lib/isBase64.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/isBase64.js b/src/lib/isBase64.js index 95fc5960e..54b9bbaa6 100644 --- a/src/lib/isBase64.js +++ b/src/lib/isBase64.js @@ -32,7 +32,6 @@ export default function isBase64(str, options) { if (paddingCount > 2) return false; const dataLen = len - paddingCount; - if (dataLen === 0) return false; for (let i = 0; i < dataLen; i++) { if (!isValidBase64Char(str.charCodeAt(i), options.urlSafe)) return false; From a962534dcb3797f90d1a8b1264bb907ea0ca7ed0 Mon Sep 17 00:00:00 2001 From: easedu Date: Sat, 7 Mar 2026 19:42:47 -0300 Subject: [PATCH 3/3] revert implementation, keep large-string tests only Per maintainer feedback: reverted isBase64.js to original regex implementation. Kept new large-string test cases for regression coverage. --- src/lib/isBase64.js | 40 +++++++++------------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/src/lib/isBase64.js b/src/lib/isBase64.js index 54b9bbaa6..fd876e4c0 100644 --- a/src/lib/isBase64.js +++ b/src/lib/isBase64.js @@ -1,18 +1,10 @@ import assertString from './util/assertString'; import merge from './util/merge'; -function isValidBase64Char(code, urlSafe) { - // A-Z (65-90), a-z (97-122), 0-9 (48-57) - if ((code >= 65 && code <= 90) - || (code >= 97 && code <= 122) - || (code >= 48 && code <= 57)) { - return true; - } - if (urlSafe) { - return code === 45 || code === 95; // - _ - } - return code === 43 || code === 47; // + / -} +const base64WithPadding = /^[A-Za-z0-9+/]+={0,2}$/; +const base64WithoutPadding = /^[A-Za-z0-9+/]+$/; +const base64UrlWithPadding = /^[A-Za-z0-9_-]+={0,2}$/; +const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/; export default function isBase64(str, options) { assertString(str); @@ -22,26 +14,12 @@ export default function isBase64(str, options) { if (options.padding && str.length % 4 !== 0) return false; - if (options.padding) { - // Count trailing '=' padding - let paddingCount = 0; - let len = str.length; - while (paddingCount < len && str.charCodeAt(len - 1 - paddingCount) === 61) { - paddingCount += 1; - } - if (paddingCount > 2) return false; - - const dataLen = len - paddingCount; - - for (let i = 0; i < dataLen; i++) { - if (!isValidBase64Char(str.charCodeAt(i), options.urlSafe)) return false; - } + let regex; + if (options.urlSafe) { + regex = options.padding ? base64UrlWithPadding : base64UrlWithoutPadding; } else { - // No padding allowed — all chars must be valid base64 - for (let i = 0; i < str.length; i++) { - if (!isValidBase64Char(str.charCodeAt(i), options.urlSafe)) return false; - } + regex = options.padding ? base64WithPadding : base64WithoutPadding; } - return true; + return (!options.padding || str.length % 4 === 0) && regex.test(str); }