Skip to content

Commit 527e435

Browse files
committed
buffer: fix DoS vector in atob
1 parent 9448c61 commit 527e435

File tree

2 files changed

+97
-31
lines changed

2 files changed

+97
-31
lines changed

lib/buffer.js

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@
2323

2424
const {
2525
Array,
26-
ArrayFrom,
2726
ArrayIsArray,
2827
ArrayPrototypeForEach,
29-
ArrayPrototypeIndexOf,
3028
MathFloor,
3129
MathMin,
3230
MathTrunc,
@@ -1255,35 +1253,41 @@ function btoa(input) {
12551253
throw new ERR_MISSING_ARGS('input');
12561254
}
12571255
input = `${input}`;
1256+
let acc = 0;
12581257
for (let n = 0; n < input.length; n++) {
1259-
if (input[n].charCodeAt(0) > 0xff)
1260-
throw lazyDOMException('Invalid character', 'InvalidCharacterError');
1258+
acc |= StringPrototypeCharCodeAt(input, n);
1259+
}
1260+
if (acc & ~0xff) {
1261+
throw lazyDOMException('Invalid character', 'InvalidCharacterError');
12611262
}
12621263
const buf = Buffer.from(input, 'latin1');
12631264
return buf.toString('base64');
12641265
}
12651266

12661267
// Refs: https://infra.spec.whatwg.org/#forgiving-base64-decode
1268+
// https://infra.spec.whatwg.org/#ascii-whitespace
1269+
// Valid Characters: [\t\n\f\r +/0-9=A-Za-z]
1270+
// Lookup table (-1 = invalid, 0 = valid)
1271+
/* eslint-disable no-multi-spaces, indent */
12671272
const kForgivingBase64AllowedChars = [
1268-
// ASCII whitespace
1269-
// Refs: https://infra.spec.whatwg.org/#ascii-whitespace
1270-
0x09, 0x0A, 0x0C, 0x0D, 0x20,
1271-
1272-
// Uppercase letters
1273-
...ArrayFrom({ length: 26 }, (_, i) => StringPrototypeCharCodeAt('A') + i),
1274-
1275-
// Lowercase letters
1276-
...ArrayFrom({ length: 26 }, (_, i) => StringPrototypeCharCodeAt('a') + i),
1277-
1278-
// Decimal digits
1279-
...ArrayFrom({ length: 10 }, (_, i) => StringPrototypeCharCodeAt('0') + i),
1280-
1281-
0x2B, // +
1282-
0x2F, // /
1283-
0x3D, // =
1273+
-1, -1, -1, -1, -1, -1, -1, -1,
1274+
-1, 0, 0, -1, 0, 0, -1, -1,
1275+
-1, -1, -1, -1, -1, -1, -1, -1,
1276+
-1, -1, -1, -1, -1, -1, -1, -1,
1277+
0, -1, -1, -1, -1, -1, -1, -1,
1278+
-1, -1, -1, 0, -1, -1, -1, 0,
1279+
0, 0, 0, 0, 0, 0, 0, 0,
1280+
0, 0, -1, -1, -1, 0, -1, -1,
1281+
-1, 0, 0, 0, 0, 0, 0, 0,
1282+
0, 0, 0, 0, 0, 0, 0, 0,
1283+
0, 0, 0, 0, 0, 0, 0, 0,
1284+
0, 0, 0, -1, -1, -1, -1, -1,
1285+
-1, 0, 0, 0, 0, 0, 0, 0,
1286+
0, 0, 0, 0, 0, 0, 0, 0,
1287+
0, 0, 0, 0, 0, 0, 0, 0,
1288+
0, 0, 0, -1, -1, -1, -1, -1,
12841289
];
1285-
const kEqualSignIndex = ArrayPrototypeIndexOf(kForgivingBase64AllowedChars,
1286-
0x3D);
1290+
/* eslint-enable no-multi-spaces, indent */
12871291

12881292
function atob(input) {
12891293
// The implementation here has not been performance optimized in any way and
@@ -1298,16 +1302,17 @@ function atob(input) {
12981302
let equalCharCount = 0;
12991303

13001304
for (let n = 0; n < input.length; n++) {
1301-
const index = ArrayPrototypeIndexOf(
1302-
kForgivingBase64AllowedChars,
1303-
StringPrototypeCharCodeAt(input, n));
1305+
const ch = StringPrototypeCharCodeAt(input, n);
1306+
const val = kForgivingBase64AllowedChars[ch & 0x7f];
13041307

1305-
if (index > 4) {
1306-
// The first 5 elements of `kForgivingBase64AllowedChars` are
1307-
// ASCII whitespace char codes.
1308+
if ((ch | val) & ~0x7f) {
1309+
throw lazyDOMException('Invalid character', 'InvalidCharacterError');
1310+
}
1311+
1312+
if (ch > 0x20) {
13081313
nonAsciiWhitespaceCharCount++;
13091314

1310-
if (index === kEqualSignIndex) {
1315+
if (ch === 0x3d) {
13111316
equalCharCount++;
13121317
} else if (equalCharCount) {
13131318
// The `=` char is only allowed at the end.
@@ -1318,8 +1323,6 @@ function atob(input) {
13181323
// Only one more `=` is permitted after the first equal sign.
13191324
throw lazyDOMException('Invalid character', 'InvalidCharacterError');
13201325
}
1321-
} else if (index === -1) {
1322-
throw lazyDOMException('Invalid character', 'InvalidCharacterError');
13231326
}
13241327
}
13251328

test/parallel/test-btoa-atob.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,69 @@ strictEqual(atob([]), '');
2525
strictEqual(atob({ toString: () => '' }), '');
2626
strictEqual(atob({ [Symbol.toPrimitive]: () => '' }), '');
2727

28+
const invalidChar = {
29+
name: 'InvalidCharacterError'
30+
};
31+
32+
// Test the entire 16 bit space for invalid characters.
33+
for (let i = 0; i <= 0xffff; i++) {
34+
switch (i) {
35+
case 0x09: // \t
36+
case 0x0A: // \n
37+
case 0x0C: // \f
38+
case 0x0D: // \r
39+
case 0x20: // ' '
40+
case 0x2B: // +
41+
case 0x2F: // /
42+
case 0x3D: // =
43+
continue;
44+
}
45+
46+
// 0-9
47+
if (i >= 0x30 && i <= 0x39)
48+
continue;
49+
50+
// A-Z
51+
if (i >= 0x41 && i <= 0x5a)
52+
continue;
53+
54+
// a-z
55+
if (i >= 0x61 && i <= 0x7a)
56+
continue;
57+
58+
const ch = String.fromCharCode(i);
59+
60+
throws(() => atob(ch), invalidChar);
61+
throws(() => atob('a' + ch), invalidChar);
62+
throws(() => atob('aa' + ch), invalidChar);
63+
throws(() => atob('aaa' + ch), invalidChar);
64+
throws(() => atob(ch + 'a'), invalidChar);
65+
throws(() => atob(ch + 'aa'), invalidChar);
66+
throws(() => atob(ch + 'aaa'), invalidChar);
67+
}
68+
69+
throws(() => btoa('abcd\ufeffx'), invalidChar);
70+
71+
const charset =
72+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
73+
74+
function randomString(size) {
75+
let str = '';
76+
77+
for (let i = 0; i < size; i++)
78+
str += charset[Math.random() * charset.length | 0];
79+
80+
while (str.length & 3)
81+
str += '=';
82+
83+
return str;
84+
}
85+
86+
for (let i = 0; i < 100; i++) {
87+
const str = randomString(200);
88+
strictEqual(btoa(atob(str)), str);
89+
}
90+
2891
throws(() => atob(Symbol()), /TypeError/);
2992
[
3093
undefined, false, () => {}, {}, [1],

0 commit comments

Comments
 (0)