Skip to content

Commit ca761ea

Browse files
committed
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
1 parent b1aea75 commit ca761ea

2 files changed

Lines changed: 52 additions & 9 deletions

File tree

src/lib/isBase64.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import assertString from './util/assertString';
22
import merge from './util/merge';
33

4-
const base64WithPadding = /^[A-Za-z0-9+/]+={0,2}$/;
5-
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
6-
const base64UrlWithPadding = /^[A-Za-z0-9_-]+={0,2}$/;
7-
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
4+
function isValidBase64Char(code, urlSafe) {
5+
// A-Z (65-90), a-z (97-122), 0-9 (48-57)
6+
if ((code >= 65 && code <= 90)
7+
|| (code >= 97 && code <= 122)
8+
|| (code >= 48 && code <= 57)) {
9+
return true;
10+
}
11+
if (urlSafe) {
12+
return code === 45 || code === 95; // - _
13+
}
14+
return code === 43 || code === 47; // + /
15+
}
816

917
export default function isBase64(str, options) {
1018
assertString(str);
@@ -14,12 +22,27 @@ export default function isBase64(str, options) {
1422

1523
if (options.padding && str.length % 4 !== 0) return false;
1624

17-
let regex;
18-
if (options.urlSafe) {
19-
regex = options.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
25+
if (options.padding) {
26+
// Count trailing '=' padding
27+
let paddingCount = 0;
28+
let len = str.length;
29+
while (paddingCount < len && str.charCodeAt(len - 1 - paddingCount) === 61) {
30+
paddingCount += 1;
31+
}
32+
if (paddingCount > 2) return false;
33+
34+
const dataLen = len - paddingCount;
35+
if (dataLen === 0) return false;
36+
37+
for (let i = 0; i < dataLen; i++) {
38+
if (!isValidBase64Char(str.charCodeAt(i), options.urlSafe)) return false;
39+
}
2040
} else {
21-
regex = options.padding ? base64WithPadding : base64WithoutPadding;
41+
// No padding allowed — all chars must be valid base64
42+
for (let i = 0; i < str.length; i++) {
43+
if (!isValidBase64Char(str.charCodeAt(i), options.urlSafe)) return false;
44+
}
2245
}
2346

24-
return (!options.padding || str.length % 4 === 0) && regex.test(str);
47+
return true;
2548
}

test/validators/isBase64.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,24 @@ describe('isBase64', () => {
198198
],
199199
});
200200
});
201+
202+
it('should not cause stack overflow on large strings', () => {
203+
// Valid base64 ~1MB
204+
const largeValid = Buffer.alloc(1000000).toString('base64');
205+
if (!validator.isBase64(largeValid)) {
206+
throw new Error('isBase64() failed for a large valid base64 string');
207+
}
208+
209+
// Invalid: large base64 with an invalid character in the middle
210+
const largeInvalid = `${largeValid.slice(0, 500000)}!${largeValid.slice(500001)}`;
211+
if (validator.isBase64(largeInvalid)) {
212+
throw new Error('isBase64() should have failed for a large invalid base64 string');
213+
}
214+
215+
// URL-safe variant
216+
const largeUrlSafe = largeValid.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
217+
if (!validator.isBase64(largeUrlSafe, { urlSafe: true })) {
218+
throw new Error('isBase64() failed for a large valid base64url string');
219+
}
220+
});
201221
});

0 commit comments

Comments
 (0)