Skip to content

Commit

Permalink
feat: improve base64 validation based on RFC4648
Browse files Browse the repository at this point in the history
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
  • Loading branch information
aseyfpour committed Nov 2, 2024
1 parent fc31e6e commit b01a860
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 121 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Validator | Description
**isAscii(str)** | check if the string contains ASCII chars only.
**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].
**isBase58(str)** | check if the string is base58 encoded.
**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].
**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].
**isBefore(str [, date])** | check if the string is a date that is before the specified date.
**isBIC(str)** | check if the string is a BIC (Bank Identification Code) or SWIFT code.
**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']).
Expand Down
29 changes: 12 additions & 17 deletions src/lib/isBase64.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import assertString from './util/assertString';
import merge from './util/merge';

const notBase64 = /[^A-Z0-9+\/=]/i;
const urlSafeBase64 = /^[A-Z0-9_\-]*$/i;

const defaultBase64Options = {
urlSafe: false,
};
const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;

export default function isBase64(str, options) {
assertString(str);
options = merge(options, defaultBase64Options);
const len = str.length;
options = merge(options, { urlSafe: false, padding: !options?.urlSafe });

if (options.urlSafe) {
return urlSafeBase64.test(str);
}
if (str === '') return true;

if (len % 4 !== 0 || notBase64.test(str)) {
return false;
let regex;
if (options.urlSafe) {
regex = options.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
} else {
regex = options.padding ? base64WithPadding : base64WithoutPadding;
}

const firstPaddingChar = str.indexOf('=');
return firstPaddingChar === -1 ||
firstPaddingChar === len - 1 ||
(firstPaddingChar === len - 2 && str[len - 1] === '=');
return (!options.padding || str.length % 4 === 0) && regex.test(str);
}
103 changes: 0 additions & 103 deletions test/validators.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import assert from 'assert';
import fs from 'fs';
import timezone_mock from 'timezone-mock';
import { format } from 'util';
import vm from 'vm';
import validator from '../src/index';
import test from './testFunctions';

let validator_js = fs.readFileSync(require.resolve('../validator.js')).toString();
Expand Down Expand Up @@ -7103,76 +7101,6 @@ describe('Validators', () => {
});
});

it('should validate base64 strings', () => {
test({
validator: 'isBase64',
valid: [
'',
'Zg==',
'Zm8=',
'Zm9v',
'Zm9vYg==',
'Zm9vYmE=',
'Zm9vYmFy',
'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=',
'Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==',
'U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==',
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw' +
'UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye' +
'rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619' +
'FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx' +
'QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ' +
'Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ' +
'HQIDAQAB',
],
invalid: [
'12345',
'Vml2YW11cyBmZXJtZtesting123',
'Zg=',
'Z===',
'Zm=8',
'=m9vYg==',
'Zm9vYmFy====',
],
});

test({
validator: 'isBase64',
args: [{ urlSafe: true }],
valid: [
'',
'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
'1234',
'bXVtLW5ldmVyLXByb3Vk',
'PDw_Pz8-Pg',
'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
],
invalid: [
' AA',
'\tAA',
'\rAA',
'\nAA',
'This+isa/bad+base64Url==',
'0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
],
error: [
null,
undefined,
{},
[],
42,
],
});

for (let i = 0, str = '', encoded; i < 1000; i++) {
str += String.fromCharCode(Math.random() * 26 | 97); // eslint-disable-line no-bitwise
encoded = Buffer.from(str).toString('base64');
if (!validator.isBase64(encoded)) {
let msg = format('validator.isBase64() failed with "%s"', encoded);
throw new Error(msg);
}
}
});

it('should validate hex-encoded MongoDB ObjectId', () => {
test({
Expand Down Expand Up @@ -13703,37 +13631,6 @@ describe('Validators', () => {
});
});

it('should validate base64URL', () => {
test({
validator: 'isBase64',
args: [{ urlSafe: true }],
valid: [
'',
'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
'1234',
'bXVtLW5ldmVyLXByb3Vk',
'PDw_Pz8-Pg',
'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
],
invalid: [
' AA',
'\tAA',
'\rAA',
'\nAA',
'123=',
'This+isa/bad+base64Url==',
'0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
],
error: [
null,
undefined,
{},
[],
42,
],
});
});

it('should validate date', () => {
test({
validator: 'isDate',
Expand Down
201 changes: 201 additions & 0 deletions test/validators/isBase64.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { format } from 'util';
import test from '../testFunctions';
import validator from '../../src';

describe('isBase64', () => {
it('should validate base64 strings with default options', () => {
test({
validator: 'isBase64',
valid: [
'',
'Zg==',
'Zm8=',
'Zm9v',
'Zm9vYg==',
'Zm9vYmE=',
'Zm9vYmFy',
'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=',
'Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==',
'U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==',
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw' +
'UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye' +
'rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619' +
'FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx' +
'QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ' +
'Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ' +
'HQIDAQAB',
],
invalid: [
'12345',
'Vml2YW11cyBmZXJtZtesting123',
'Zg=',
'Z===',
'Zm=8',
'=m9vYg==',
'Zm9vYmFy====',
],
});

test({
validator: 'isBase64',
args: [{ urlSafe: true }],
valid: [
'',
'bGFkaWVzIGFuZCBnZW50bGVtZW4sIHdlIGFyZSBmbG9hdGluZyBpbiBzcGFjZQ',
'1234',
'bXVtLW5ldmVyLXByb3Vk',
'PDw_Pz8-Pg',
'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw',
],
invalid: [
' AA',
'\tAA',
'\rAA',
'\nAA',
'This+isa/bad+base64Url==',
'0K3RgtC+INC30LDQutC+0LTQuNGA0L7QstCw0L3QvdCw0Y8g0YHRgtGA0L7QutCw',
],
error: [
null,
undefined,
{},
[],
42,
],
});

for (let i = 0, str = '', encoded; i < 1000; i++) {
str += String.fromCharCode(Math.random() * 26 | 97); // eslint-disable-line no-bitwise
encoded = Buffer.from(str).toString('base64');
if (!validator.isBase64(encoded)) {
let msg = format('validator.isBase64() failed with "%s"', encoded);
throw new Error(msg);
}
}
});

it('should validate standard Base64 with padding', () => {
test({
validator: 'isBase64',
args: [{ urlSafe: false, padding: true }],
valid: [
'',
'TWFu',
'TWE=',
'TQ==',
'SGVsbG8=',
'U29mdHdhcmU=',
'YW55IGNhcm5hbCBwbGVhc3VyZS4=',
],
invalid: [
'TWF',
'TWE===',
'SGVsbG8@',
'SGVsbG8===',
'SGVsb G8=',
'====',
],
});
});

it('should validate standard Base64 without padding', () => {
test({
validator: 'isBase64',
args: [{ urlSafe: false, padding: false }],
valid: [
'',
'TWFu',
'TWE',
'TQ',
'SGVsbG8',
'U29mdHdhcmU',
'YW55IGNhcm5hbCBwbGVhc3VyZS4',
],
invalid: [
'TWE=',
'TQ===',
'SGVsbG8@',
'SGVsbG8===',
'SGVsb G8',
'====',
],
});
});

it('should validate Base64url with padding', () => {
test({
validator: 'isBase64',
args: [{ urlSafe: true, padding: true }],
valid: [
'',
'SGVsbG8=',
'U29mdHdhcmU=',
'YW55IGNhcm5hbCBwbGVhc3VyZS4=',
'SGVsbG8-',
'SGVsbG8_',
],
invalid: [
'SGVsbG8===',
'SGVsbG8@',
'SGVsb G8=',
'====',
],
});
});

it('should validate Base64url without padding', () => {
test({
validator: 'isBase64',
args: [{ urlSafe: true, padding: false }],
valid: [
'',
'SGVsbG8',
'U29mdHdhcmU',
'YW55IGNhcm5hbCBwbGVhc3VyZS4',
'SGVsbG8-',
'SGVsbG8_',
],
invalid: [
'SGVsbG8=',
'SGVsbG8===',
'SGVsbG8@',
'SGVsb G8',
'====',
],
});
});

it('should handle mixed cases correctly', () => {
test({
validator: 'isBase64',
args: [{ urlSafe: false, padding: true }],
valid: [
'',
'TWFu',
'TWE=',
'TQ==',
],
invalid: [
'TWE',
'TQ=',
'TQ===',
],
});

test({
validator: 'isBase64',
args: [{ urlSafe: true, padding: false }],
valid: [
'',
'SGVsbG8',
'SGVsbG8-',
'SGVsbG8_',
],
invalid: [
'SGVsbG8=',
'SGVsbG8@',
'SGVsb G8',
],
});
});
});

0 comments on commit b01a860

Please sign in to comment.