Skip to content

Commit 7ff247d

Browse files
authored
feat(isBase64): improve validation based on RFC4648 (#2491)
add padding to the option list update regexes to support validation with/without padding update default options to keep the changes backward compatible add new test to cover different scenarios
1 parent 7ab06c4 commit 7ff247d

File tree

4 files changed

+214
-121
lines changed

4 files changed

+214
-121
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Validator | Description
9393
**isAscii(str)** | check if the string contains ASCII chars only.
9494
**isBase32(str [, options])** | check if the string is base32 encoded. `options` is optional and defaults to `{ crockford: false }`.<br/> When `crockford` is true it tests the given base32 encoded string using [Crockford's base32 alternative][Crockford Base32].
9595
**isBase58(str)** | check if the string is base58 encoded.
96-
**isBase64(str [, options])** | check if the string is base64 encoded. `options` is optional and defaults to `{ urlSafe: false }`<br/> when `urlSafe` is true it tests the given base64 encoded string is [url safe][Base64 URL Safe].
96+
**isBase64(str [, options])** | check if the string is base64 encoded. `options` is optional and defaults to `{ urlSafe: false, padding: true }`<br/> when `urlSafe` is true default value for `padding` is false and it tests the given base64 encoded string is [url safe][Base64 URL Safe].
9797
**isBefore(str [, date])** | check if the string is a date that is before the specified date.
9898
**isBIC(str)** | check if the string is a BIC (Bank Identification Code) or SWIFT code.
9999
**isBoolean(str [, options])** | check if the string is a boolean.<br/>`options` is an object which defaults to `{ loose: false }`. If `loose` is set to false, the validator will strictly match ['true', 'false', '0', '1']. If `loose` is set to true, the validator will also match 'yes', 'no', and will match a valid boolean string of any case. (e.g.: ['true', 'True', 'TRUE']).

src/lib/isBase64.js

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
import assertString from './util/assertString';
22
import merge from './util/merge';
33

4-
const notBase64 = /[^A-Z0-9+\/=]/i;
5-
const urlSafeBase64 = /^[A-Z0-9_\-]*$/i;
6-
7-
const defaultBase64Options = {
8-
urlSafe: false,
9-
};
4+
const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
5+
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
6+
const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
7+
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
108

119
export default function isBase64(str, options) {
1210
assertString(str);
13-
options = merge(options, defaultBase64Options);
14-
const len = str.length;
11+
options = merge(options, { urlSafe: false, padding: !options?.urlSafe });
1512

16-
if (options.urlSafe) {
17-
return urlSafeBase64.test(str);
18-
}
13+
if (str === '') return true;
1914

20-
if (len % 4 !== 0 || notBase64.test(str)) {
21-
return false;
15+
let regex;
16+
if (options.urlSafe) {
17+
regex = options.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
18+
} else {
19+
regex = options.padding ? base64WithPadding : base64WithoutPadding;
2220
}
2321

24-
const firstPaddingChar = str.indexOf('=');
25-
return firstPaddingChar === -1 ||
26-
firstPaddingChar === len - 1 ||
27-
(firstPaddingChar === len - 2 && str[len - 1] === '=');
22+
return (!options.padding || str.length % 4 === 0) && regex.test(str);
2823
}

test/validators.test.js

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import assert from 'assert';
22
import fs from 'fs';
33
import timezone_mock from 'timezone-mock';
4-
import { format } from 'util';
54
import vm from 'vm';
6-
import validator from '../src/index';
75
import test from './testFunctions';
86

97
let validator_js = fs.readFileSync(require.resolve('../validator.js')).toString();
@@ -7025,76 +7023,6 @@ describe('Validators', () => {
70257023
});
70267024
});
70277025

7028-
it('should validate base64 strings', () => {
7029-
test({
7030-
validator: 'isBase64',
7031-
valid: [
7032-
'',
7033-
'Zg==',
7034-
'Zm8=',
7035-
'Zm9v',
7036-
'Zm9vYg==',
7037-
'Zm9vYmE=',
7038-
'Zm9vYmFy',
7039-
'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=',
7040-
'Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==',
7041-
'U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==',
7042-
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw' +
7043-
'UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye' +
7044-
'rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619' +
7045-
'FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx' +
7046-
'QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ' +
7047-
'Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ' +
7048-
'HQIDAQAB',
7049-
],
7050-
invalid: [
7051-
'12345',
7052-
'Vml2YW11cyBmZXJtZtesting123',
7053-
'Zg=',
7054-
'Z===',
7055-
'Zm=8',
7056-
'=m9vYg==',
7057-
'Zm9vYmFy====',
7058-
],
7059-
});
7060-
7061-
test({
7062-
validator: 'isBase64',
7063-
args: [{ urlSafe: true }],
7064-
valid: [
7065-
'',
7066-
'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
7067-
'1234',
7068-
'bXVtLW5ldmVyLXByb3Vk',
7069-
'PDw_Pz8-Pg',
7070-
'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
7071-
],
7072-
invalid: [
7073-
' AA',
7074-
'\tAA',
7075-
'\rAA',
7076-
'\nAA',
7077-
'This+isa/bad+base64Url==',
7078-
'0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
7079-
],
7080-
error: [
7081-
null,
7082-
undefined,
7083-
{},
7084-
[],
7085-
42,
7086-
],
7087-
});
7088-
7089-
for (let i = 0, str = '', encoded; i < 1000; i++) {
7090-
str += String.fromCharCode(Math.random() * 26 | 97); // eslint-disable-line no-bitwise
7091-
encoded = Buffer.from(str).toString('base64');
7092-
if (!validator.isBase64(encoded)) {
7093-
let msg = format('validator.isBase64() failed with "%s"', encoded);
7094-
throw new Error(msg);
7095-
}
7096-
}
7097-
});
70987026

70997027
it('should validate hex-encoded MongoDB ObjectId', () => {
71007028
test({
@@ -13740,37 +13668,6 @@ describe('Validators', () => {
1374013668
});
1374113669
});
1374213670

13743-
it('should validate base64URL', () => {
13744-
test({
13745-
validator: 'isBase64',
13746-
args: [{ urlSafe: true }],
13747-
valid: [
13748-
'',
13749-
'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
13750-
'1234',
13751-
'bXVtLW5ldmVyLXByb3Vk',
13752-
'PDw_Pz8-Pg',
13753-
'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
13754-
],
13755-
invalid: [
13756-
' AA',
13757-
'\tAA',
13758-
'\rAA',
13759-
'\nAA',
13760-
'123=',
13761-
'This+isa/bad+base64Url==',
13762-
'0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
13763-
],
13764-
error: [
13765-
null,
13766-
undefined,
13767-
{},
13768-
[],
13769-
42,
13770-
],
13771-
});
13772-
});
13773-
1377413671
it('should validate date', () => {
1377513672
test({
1377613673
validator: 'isDate',

test/validators/isBase64.test.js

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { format } from 'util';
2+
import test from '../testFunctions';
3+
import validator from '../../src';
4+
5+
describe('isBase64', () => {
6+
it('should validate base64 strings with default options', () => {
7+
test({
8+
validator: 'isBase64',
9+
valid: [
10+
'',
11+
'Zg==',
12+
'Zm8=',
13+
'Zm9v',
14+
'Zm9vYg==',
15+
'Zm9vYmE=',
16+
'Zm9vYmFy',
17+
'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=',
18+
'Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==',
19+
'U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==',
20+
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw' +
21+
'UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye' +
22+
'rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619' +
23+
'FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx' +
24+
'QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ' +
25+
'Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ' +
26+
'HQIDAQAB',
27+
],
28+
invalid: [
29+
'12345',
30+
'Vml2YW11cyBmZXJtZtesting123',
31+
'Zg=',
32+
'Z===',
33+
'Zm=8',
34+
'=m9vYg==',
35+
'Zm9vYmFy====',
36+
],
37+
});
38+
39+
test({
40+
validator: 'isBase64',
41+
args: [{ urlSafe: true }],
42+
valid: [
43+
'',
44+
'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
45+
'1234',
46+
'bXVtLW5ldmVyLXByb3Vk',
47+
'PDw_Pz8-Pg',
48+
'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
49+
],
50+
invalid: [
51+
' AA',
52+
'\tAA',
53+
'\rAA',
54+
'\nAA',
55+
'This+isa/bad+base64Url==',
56+
'0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
57+
],
58+
error: [
59+
null,
60+
undefined,
61+
{},
62+
[],
63+
42,
64+
],
65+
});
66+
67+
for (let i = 0, str = '', encoded; i < 1000; i++) {
68+
str += String.fromCharCode(Math.random() * 26 | 97); // eslint-disable-line no-bitwise
69+
encoded = Buffer.from(str).toString('base64');
70+
if (!validator.isBase64(encoded)) {
71+
let msg = format('validator.isBase64() failed with "%s"', encoded);
72+
throw new Error(msg);
73+
}
74+
}
75+
});
76+
77+
it('should validate standard Base64 with padding', () => {
78+
test({
79+
validator: 'isBase64',
80+
args: [{ urlSafe: false, padding: true }],
81+
valid: [
82+
'',
83+
'TWFu',
84+
'TWE=',
85+
'TQ==',
86+
'SGVsbG8=',
87+
'U29mdHdhcmU=',
88+
'YW55IGNhcm5hbCBwbGVhc3VyZS4=',
89+
],
90+
invalid: [
91+
'TWF',
92+
'TWE===',
93+
'SGVsbG8@',
94+
'SGVsbG8===',
95+
'SGVsb G8=',
96+
'====',
97+
],
98+
});
99+
});
100+
101+
it('should validate standard Base64 without padding', () => {
102+
test({
103+
validator: 'isBase64',
104+
args: [{ urlSafe: false, padding: false }],
105+
valid: [
106+
'',
107+
'TWFu',
108+
'TWE',
109+
'TQ',
110+
'SGVsbG8',
111+
'U29mdHdhcmU',
112+
'YW55IGNhcm5hbCBwbGVhc3VyZS4',
113+
],
114+
invalid: [
115+
'TWE=',
116+
'TQ===',
117+
'SGVsbG8@',
118+
'SGVsbG8===',
119+
'SGVsb G8',
120+
'====',
121+
],
122+
});
123+
});
124+
125+
it('should validate Base64url with padding', () => {
126+
test({
127+
validator: 'isBase64',
128+
args: [{ urlSafe: true, padding: true }],
129+
valid: [
130+
'',
131+
'SGVsbG8=',
132+
'U29mdHdhcmU=',
133+
'YW55IGNhcm5hbCBwbGVhc3VyZS4=',
134+
'SGVsbG8-',
135+
'SGVsbG8_',
136+
],
137+
invalid: [
138+
'SGVsbG8===',
139+
'SGVsbG8@',
140+
'SGVsb G8=',
141+
'====',
142+
],
143+
});
144+
});
145+
146+
it('should validate Base64url without padding', () => {
147+
test({
148+
validator: 'isBase64',
149+
args: [{ urlSafe: true, padding: false }],
150+
valid: [
151+
'',
152+
'SGVsbG8',
153+
'U29mdHdhcmU',
154+
'YW55IGNhcm5hbCBwbGVhc3VyZS4',
155+
'SGVsbG8-',
156+
'SGVsbG8_',
157+
],
158+
invalid: [
159+
'SGVsbG8=',
160+
'SGVsbG8===',
161+
'SGVsbG8@',
162+
'SGVsb G8',
163+
'====',
164+
],
165+
});
166+
});
167+
168+
it('should handle mixed cases correctly', () => {
169+
test({
170+
validator: 'isBase64',
171+
args: [{ urlSafe: false, padding: true }],
172+
valid: [
173+
'',
174+
'TWFu',
175+
'TWE=',
176+
'TQ==',
177+
],
178+
invalid: [
179+
'TWE',
180+
'TQ=',
181+
'TQ===',
182+
],
183+
});
184+
185+
test({
186+
validator: 'isBase64',
187+
args: [{ urlSafe: true, padding: false }],
188+
valid: [
189+
'',
190+
'SGVsbG8',
191+
'SGVsbG8-',
192+
'SGVsbG8_',
193+
],
194+
invalid: [
195+
'SGVsbG8=',
196+
'SGVsbG8@',
197+
'SGVsb G8',
198+
],
199+
});
200+
});
201+
});

0 commit comments

Comments
 (0)