From f2209a8cfbc0e4ee5c46ad43224552d93025237e Mon Sep 17 00:00:00 2001 From: maximilliangrand <214999687+maximilliangrand@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:34:18 +0200 Subject: [PATCH] fix(isBase32): reject impossible padding lengths RFC 4648 section 6 encodes each 40-bit group as 8 characters. A final group of 1, 2, 3 or 4 octets encodes to 2, 4, 5 or 7 characters and is padded to 8, so a conforming encoder emits 0, 1, 3, 4 or 6 padding characters - never 2, 5 or 7. isBase32 only checked that the length is a multiple of 8 and that the string matches /^[A-Z2-7]+=*$/, so it accepted shapes no encoder can produce and no decoder accepts, e.g. 'JBSWY3==' (2 padding characters), 'JBS=====' (5) and 'J=======' (7). It also accepted an arbitrarily long padding run such as 'A' followed by 1048575 '=' characters. Add an explicit padding-length check. Every string an RFC 4648 encoder can produce is still accepted; the change only removes shapes that are unreachable under the standard. --- src/lib/isBase32.js | 19 ++++++++++++++++++- test/validators.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/lib/isBase32.js b/src/lib/isBase32.js index 81d30c9f5..080a2f429 100644 --- a/src/lib/isBase32.js +++ b/src/lib/isBase32.js @@ -1,13 +1,30 @@ import assertString from './util/assertString'; +import includes from './util/includesArray'; import merge from './util/merge'; const base32 = /^[A-Z2-7]+=*$/; const crockfordBase32 = /^[A-HJKMNP-TV-Z0-9]+$/; +/** + * RFC 4648 (section 6) encodes each 40-bit group as 8 characters. A final + * group of 1, 2, 3 or 4 octets encodes to 2, 4, 5 or 7 characters and is then + * padded to 8, so an encoder only ever emits 0, 1, 3, 4 or 6 padding + * characters - never 2, 5 or 7. + */ +const validPaddingLengths = [0, 1, 3, 4, 6]; + const defaultBase32Options = { crockford: false, }; +/* `str` has already been matched against `base32`, so any '=' is part of the + single trailing padding run. */ +function hasValidPadding(str) { + const paddingStart = str.indexOf('='); + + return includes(validPaddingLengths, paddingStart === -1 ? 0 : str.length - paddingStart); +} + export default function isBase32(str, options) { assertString(str); options = merge(options, defaultBase32Options); @@ -16,5 +33,5 @@ export default function isBase32(str, options) { return crockfordBase32.test(str); } - return str.length % 8 === 0 && base32.test(str); + return str.length % 8 === 0 && base32.test(str) && hasValidPadding(str); } diff --git a/test/validators.test.js b/test/validators.test.js index 3d2c8e8b2..a57e2922c 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -7503,6 +7503,31 @@ describe('Validators', () => { }); }); + it('should reject base32 strings with an impossible number of padding characters', () => { + // RFC 4648 (section 6) pads the final group to 8 characters, so an encoder + // emits 0, 1, 3, 4 or 6 padding characters - never 2, 5 or 7. + test({ + validator: 'isBase32', + valid: [ + 'ZG======', + 'JBSQ====', + 'JBSWY===', + 'JBSWY3A=', + 'JBSWY3DP', + 'JBSWY3DPEA======', + ], + invalid: [ + 'JBSWY3==', + 'JBS=====', + 'J=======', + 'JBSWY3DPJBSWY3==', + 'JBSWY3DPJBS=====', + 'JBSWY3DPJ=======', + 'JBSWY3==========', + ], + }); + }); + it('should validate base32 strings with crockford alternative', () => { test({ validator: 'isBase32',