Skip to content

Commit b01a860

Browse files
committed
feat: improve base64 validation based on RFC4648
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 fc31e6e commit b01a860

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();
@@ -7103,76 +7101,6 @@ describe('Validators', () => {
71037101
});
71047102
});
71057103

7106-
it('should validate base64 strings', () => {
7107-
test({
7108-
validator: 'isBase64',
7109-
valid: [
7110-
'',
7111-
'Zg==',
7112-
'Zm8=',
7113-
'Zm9v',
7114-
'Zm9vYg==',
7115-
'Zm9vYmE=',
7116-
'Zm9vYmFy',
7117-
'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=',
7118-
'Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==',
7119-
'U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==',
7120-
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw' +
7121-
'UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye' +
7122-
'rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619' +
7123-
'FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx' +
7124-
'QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ' +
7125-
'Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ' +
7126-
'HQIDAQAB',
7127-
],
7128-
invalid: [
7129-
'12345',
7130-
'Vml2YW11cyBmZXJtZtesting123',
7131-
'Zg=',
7132-
'Z===',
7133-
'Zm=8',
7134-
'=m9vYg==',
7135-
'Zm9vYmFy====',
7136-
],
7137-
});
7138-
7139-
test({
7140-
validator: 'isBase64',
7141-
args: [{ urlSafe: true }],
7142-
valid: [
7143-
'',
7144-
'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
7145-
'1234',
7146-
'bXVtLW5ldmVyLXByb3Vk',
7147-
'PDw_Pz8-Pg',
7148-
'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
7149-
],
7150-
invalid: [
7151-
' AA',
7152-
'\tAA',
7153-
'\rAA',
7154-
'\nAA',
7155-
'This+isa/bad+base64Url==',
7156-
'0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
7157-
],
7158-
error: [
7159-
null,
7160-
undefined,
7161-
{},
7162-
[],
7163-
42,
7164-
],
7165-
});
7166-
7167-
for (let i = 0, str = '', encoded; i < 1000; i++) {
7168-
str += String.fromCharCode(Math.random() * 26 | 97); // eslint-disable-line no-bitwise
7169-
encoded = Buffer.from(str).toString('base64');
7170-
if (!validator.isBase64(encoded)) {
7171-
let msg = format('validator.isBase64() failed with "%s"', encoded);
7172-
throw new Error(msg);
7173-
}
7174-
}
7175-
});
71767104

71777105
it('should validate hex-encoded MongoDB ObjectId', () => {
71787106
test({
@@ -13703,37 +13631,6 @@ describe('Validators', () => {
1370313631
});
1370413632
});
1370513633

13706-
it('should validate base64URL', () => {
13707-
test({
13708-
validator: 'isBase64',
13709-
args: [{ urlSafe: true }],
13710-
valid: [
13711-
'',
13712-
'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
13713-
'1234',
13714-
'bXVtLW5ldmVyLXByb3Vk',
13715-
'PDw_Pz8-Pg',
13716-
'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
13717-
],
13718-
invalid: [
13719-
' AA',
13720-
'\tAA',
13721-
'\rAA',
13722-
'\nAA',
13723-
'123=',
13724-
'This+isa/bad+base64Url==',
13725-
'0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
13726-
],
13727-
error: [
13728-
null,
13729-
undefined,
13730-
{},
13731-
[],
13732-
42,
13733-
],
13734-
});
13735-
});
13736-
1373713634
it('should validate date', () => {
1373813635
test({
1373913636
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)