From b01a860f7d40bb01ae0bb2aa30e99e06bedb2f5b Mon Sep 17 00:00:00 2001 From: AliReza Seyfpour Date: Sat, 2 Nov 2024 10:56:20 +0330 Subject: [PATCH] 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 --- README.md | 2 +- src/lib/isBase64.js | 29 ++--- test/validators.test.js | 103 ---------------- test/validators/isBase64.test.js | 201 +++++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 121 deletions(-) create mode 100644 test/validators/isBase64.test.js diff --git a/README.md b/README.md index c7518fd41..7a8bc14f6 100644 --- a/README.md +++ b/README.md @@ -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 }`.
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 }`
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 }`
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.
`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']). diff --git a/src/lib/isBase64.js b/src/lib/isBase64.js index 02dead0f4..7eb3a5b56 100644 --- a/src/lib/isBase64.js +++ b/src/lib/isBase64.js @@ -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); } diff --git a/test/validators.test.js b/test/validators.test.js index 08d76f821..4aeed5b3a 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -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(); @@ -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({ @@ -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', diff --git a/test/validators/isBase64.test.js b/test/validators/isBase64.test.js new file mode 100644 index 000000000..c0074343a --- /dev/null +++ b/test/validators/isBase64.test.js @@ -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', + ], + }); + }); +});