diff --git a/src/lib/isBase64.js b/src/lib/isBase64.js index fd876e4c0..830e15e93 100644 --- a/src/lib/isBase64.js +++ b/src/lib/isBase64.js @@ -6,13 +6,24 @@ const base64WithoutPadding = /^[A-Za-z0-9+/]+$/; const base64UrlWithPadding = /^[A-Za-z0-9_-]+={0,2}$/; const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/; +// When padding is required, the length must be a multiple of 4. +// When padding is omitted, the length must be one of the valid +// unpadded base64 lengths: 0, 2, 3, 4, 6, 7, 8, 10, 11, 12, 14, 15, 16, ... +// i.e. (length % 4) is 0, 2, or 3 — never 1. Lengths of the form +// (4k+1) cannot be produced by any base64 encoding of any byte string. +const validUnpaddedLength = (n) => n % 4 !== 1; + export default function isBase64(str, options) { assertString(str); options = merge(options, { urlSafe: false, padding: !options?.urlSafe }); if (str === '') return true; - if (options.padding && str.length % 4 !== 0) return false; + if (options.padding) { + if (str.length % 4 !== 0) return false; + } else if (!validUnpaddedLength(str.length)) { + return false; + } let regex; if (options.urlSafe) { @@ -21,5 +32,5 @@ export default function isBase64(str, options) { regex = options.padding ? base64WithPadding : base64WithoutPadding; } - return (!options.padding || str.length % 4 === 0) && regex.test(str); + return regex.test(str); } diff --git a/test/validators/isBase64.test.js b/test/validators/isBase64.test.js index c0074343a..6f8d22100 100644 --- a/test/validators/isBase64.test.js +++ b/test/validators/isBase64.test.js @@ -198,4 +198,48 @@ describe('isBase64', () => { ], }); }); + + + it('should reject strings whose length is not a valid base64 length when padding is off', () => { + // Regression: with padding: false, isBase64 was returning true for + // ANY length that matched the character regex, including 1 char + // ('A'), 5 chars ('AAAAA'), 9 chars, etc. A base64 string without + // padding must have length L where L % 4 is 0, 2, or 3 — never 1. + // A string whose length leaves remainder 1 when divided by 4 has + // no valid decoding. + test({ + validator: 'isBase64', + args: [{ urlSafe: false, padding: false }], + valid: [ + '', + 'TQ', + 'TWE', + 'TWFu', + 'TU0', + 'TU1NTQ', + 'TU1NTU0', + 'TU1NTU1N', + ], + invalid: [ + 'A', + 'AAAAA', + 'AAAAAAAAA', + 'AAAAAAAAAAAAA', + ], + }); + + test({ + validator: 'isBase64', + args: [{ urlSafe: true, padding: false }], + valid: [ + 'PDw_Pz8-Pg', + 'bGFkaWVz', + ], + invalid: [ + 'A', + 'AAAAA', + 'AAAAAAAAA', + ], + }); + }); });