fix(isBase32): reject impossible padding lengths - #2852
Open
maximilliangrand wants to merge 1 commit into
Open
Conversation
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.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2852 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 114 114
Lines 2599 2604 +5
Branches 658 659 +1
=========================================
+ Hits 2599 2604 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
isBase32accepts strings that no RFC 4648 encoder can produce and no RFC 4648 decoder will accept.The contract
RFC 4648 section 6 encodes each 40-bit (5-octet) group as 8 characters. When the final group is short, it is zero-extended and then padded with
=to a full 8 characters:=charactersSo the number of padding characters in a well-formed base32 string is always 0, 1, 3, 4 or 6 — never 2, 5 or 7. (This is why every
validfixture already in the suite has a padding run of 0, 1, 3, 4 or 6 —JBSWY3DP,JBSWY3A=,JBSWY===,JBSQ====,ZG======,JBSWY3DPEA======,K5SWYY3PNVSSA5DPEBXG6ZA=andK5SWYY3PNVSSA5DPEBXG6===cover exactly those five lengths and no others.)isBase32only enforces two of the three rules: the alphabet and the "length is a multiple of 8" rule. The padding-length rule is missing, so padding runs of 2, 5 and 7 characters are accepted even though no encoder can emit them — as is any longer run, provided the total length stays a multiple of 8.Reproduction (published release,
validator@13.15.35)Observed output:
An independent decoder rejects all three (Python 3 stdlib, same RFC):
The same gap also lets an unbounded padding run through, as long as the total length stays a multiple of 8:
Root cause
src/lib/isBase32.js:19with
base32 = /^[A-Z2-7]+=*$/(line 4).=*places no constraint on how many padding characters there are, and the% 8check only constrains the total length, soJBSWY3==(6 data + 2 pad) andJ=======(1 data + 7 pad) satisfy both conditions.For comparison,
isBase64already enforces the analogous rule: its patterns end in={0,2}, which is exactly the padding set RFC 4648 section 4 permits for base64 ({0, 1, 2}) —isBase64('a===')is correctlyfalse.isBase32uses an unbounded=*instead, so the base32 path is missing a constraint its base64 sibling already applies.Fix
Add the missing rule as an explicit check rather than folding it into the regex.
isBase64had a regex-driven stack overflow on large inputs (#2573, fixed in #2574 by separating the length check from the pattern), so this deliberately avoids introducing a nested quantifier such as(?:[A-Z2-7]{8})*…;indexOfis linear and allocation-free, and the character-class regex is unchanged.hasValidPaddingruns last, afterbase32.test(str)has already guaranteed that every=belongs to a single trailing run.Evidence
Differential check against an independent RFC 4648 §6 encoder written for this report:
Fail-then-pass, against a rebuilt baseline (
src/lib/isBase32.jsrestored fromorigin/master,npm run buildre-run, generatedlib/,es/andvalidator.jsconfirmed not to contain the new symbol):With the fix (
lib/isBase32.js,es/lib/isBase32.jsandvalidator.jsall rebuilt and containinghasValidPadding):Full
npm test(builds all distributions, lints, then runs the suite): 324 passing, 0 failing (323 before this change). Statements/lines/functions coverage stays at 100%; branch coverage unchanged at 96.59%.Regression surface
Measured in both directions.
A{k}={L-k}for total lengths 8, 16 and 24 gives 33 strings whose result changes — all of themtrue -> false, and every one has a padding run of length ∉ {0, 1, 3, 4, 6}.false. None of them is producible by a conforming encoder or decodable by a conforming decoder, so no valid base32 input is affected.crockford: truepath returns before the new check and is untouched.isBase32('')remainsfalse, matching the existing fixture.Security / performance
No regex was added or modified, so there is no new backtracking surface. The added work is one
String.prototype.indexOfplus a five-element array scan. Timed over adversarial inputs (20 iterations each, before vs. after):'A'+ 1 048 575'='false)'=A'What I did not verify / deliberately left alone
ZH======has non-zero trailing bits. That is a separate rule from padding length and a separate behaviour change, so it is out of scope here.isBase32('')remainsfalse— both are covered by existing fixtures and unchanged.crockfordbranch was touched.String.prototype.indexOfandArray.prototype.some(via the existingutil/includesArrayhelper), both ES5, and the transpiledlib/isBase32.jscontains no ES6+ syntax.Note on #2698
Open PR #2698 splits
test/validators.test.jsinto per-function files. If it lands first, the newit()block here needs to move totest/validators/isBase32.test.js— happy to rebase whichever way round is convenient.References
feat(isBase64): improve base64 validation based on RFC4648, merged)Checklist
isBase32as "check if the string is base32 encoded".