BUG(isJWT): validate decoded header and payload as JSON objects#2677
BUG(isJWT): validate decoded header and payload as JSON objects#2677Kartikeya-guthub wants to merge 11 commits intovalidatorjs:masterfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2677 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 114 114
Lines 2595 2615 +20
Branches 659 667 +8
=========================================
+ Hits 2595 2615 +20 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR tightens isJWT() validation to ensure JWT header and payload segments decode into valid JSON objects, addressing cases where previously any three Base64URL-like segments could pass.
Changes:
- Decode Base64URL header/payload and
JSON.parsethem to ensure they are JSON objects (not arrays/null/primitives). - Keep allowing an empty signature segment (trailing
.) for unsecured JWTs. - Extend the
isJWTinvalid test corpus with non-JSON and non-object decoded segment cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/lib/isJWT.js |
Adds base64url decode + JSON-object structural validation for header/payload. |
test/validators.test.js |
Adds additional invalid JWT strings to cover new structural validation behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/lib/isJWT.js
Outdated
| function tryDecodeJSON(segment) { | ||
| if (!isBase64(segment, { urlSafe: true })) return false; | ||
| try { | ||
| // Normalize base64url alphabet to base64, then restore stripped padding | ||
| let b64 = segment.replace(/-/g, '+').replace(/_/g, '/'); | ||
| while (b64.length % 4) b64 += '='; | ||
| const decoded = Buffer.from(b64, 'base64').toString('utf8'); |
There was a problem hiding this comment.
Buffer.from(...) is used to decode base64, but Buffer is Node-specific and Buffer.from is not available on the minimum supported Node versions (package.json declares node >= 0.10). This will also break the browser UMD build unless a Buffer polyfill is guaranteed. Consider switching to a runtime-agnostic base64url decode implementation (e.g., atob/TextDecoder in browsers with a safe fallback for old Node), or gate the Buffer path behind an availability check and provide a non-Buffer fallback.
| function tryDecodeJSON(segment) { | |
| if (!isBase64(segment, { urlSafe: true })) return false; | |
| try { | |
| // Normalize base64url alphabet to base64, then restore stripped padding | |
| let b64 = segment.replace(/-/g, '+').replace(/_/g, '/'); | |
| while (b64.length % 4) b64 += '='; | |
| const decoded = Buffer.from(b64, 'base64').toString('utf8'); | |
| function decodeBase64UrlToUtf8(b64) { | |
| // Prefer Node.js Buffer when available (supports old Node via `new Buffer`). | |
| if (typeof Buffer !== 'undefined') { | |
| if (typeof Buffer.from === 'function') { | |
| return Buffer.from(b64, 'base64').toString('utf8'); | |
| } | |
| // Fallback for very old Node versions where Buffer.from is not available. | |
| // eslint-disable-next-line no-buffer-constructor | |
| return new Buffer(b64, 'base64').toString('utf8'); | |
| } | |
| // Browser / non-Node environment: use atob/TextDecoder if available. | |
| if (typeof atob === 'function') { | |
| const binary = atob(b64); | |
| // If TextDecoder is available, use it for proper UTF-8 decoding. | |
| if (typeof TextDecoder !== 'undefined') { | |
| const bytes = new Uint8Array(binary.length); | |
| for (let i = 0; i < binary.length; i += 1) { | |
| bytes[i] = binary.charCodeAt(i); | |
| } | |
| return new TextDecoder('utf-8').decode(bytes); | |
| } | |
| // Fallback UTF-8 decoding using percent-encoding. | |
| let encoded = ''; | |
| for (let i = 0; i < binary.length; i += 1) { | |
| const code = binary.charCodeAt(i).toString(16).padStart(2, '0'); | |
| encoded += '%' + code; | |
| } | |
| return decodeURIComponent(encoded); | |
| } | |
| // As a last resort, return the input unchanged. | |
| return b64; | |
| } | |
| function tryDecodeJSON(segment) { | |
| if (!isBase64(segment, { urlSafe: true })) return false; | |
| try { | |
| // Normalize base64url alphabet to base64, then restore stripped padding | |
| let b64 = segment.replace(/-/g, '+').replace(/_/g, '/'); | |
| while (b64.length % 4) b64 += '='; | |
| const decoded = decodeBase64UrlToUtf8(b64); |
| 'foo.bar.', | ||
| '..', | ||
| '.t.', | ||
| 'foo.bar.baz', | ||
| 'Zm9v.YmFy.', |
There was a problem hiding this comment.
The PR description mentions adding a test for an unsecured JWT with an empty signature (alg: none), but this test block only adds additional invalid cases. Consider adding an explicit valid token where the header sets alg to none and the signature segment is empty (trailing dot) to ensure the intended behavior is actually covered.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/lib/isJWT.js
Outdated
| } | ||
| let encoded = ''; | ||
| for (let i = 0; i < binary.length; i += 1) { | ||
| const code = binary.charCodeAt(i).toString(16).padStart(2, '0'); |
There was a problem hiding this comment.
decodeBase64Url() uses String.prototype.padStart(). This built-in isn’t available in some older JS runtimes, and this repo targets very old Node versions and produces a browser bundle without guaranteed polyfills. Consider replacing the padStart(2, '0') usage with a small manual 2-digit hex padding implementation to avoid relying on padStart.
| const code = binary.charCodeAt(i).toString(16).padStart(2, '0'); | |
| const hex = binary.charCodeAt(i).toString(16); | |
| const code = hex.length === 1 ? `0${hex}` : hex; |
src/lib/isJWT.js
Outdated
| /* istanbul ignore next */ | ||
| function decodeBase64Url(b64) { | ||
| if (typeof Buffer !== 'undefined') { |
There was a problem hiding this comment.
/* istanbul ignore next */ on decodeBase64Url() will exclude the entire helper from coverage, even though it’s now part of isJWT()’s core behavior. Prefer removing this ignore, or scoping ignores to the genuinely untestable branches (e.g., the atob path) so Node-based tests still cover the main decoding logic.
test/validators.test.js
Outdated
| 'W10=.eyJiYXIiOiJiYXoifQ.', | ||
| 'eyJmb28iOiJiYXIifQ.W10=.', |
There was a problem hiding this comment.
Some of the newly added invalid JWT fixtures include = padding in header/payload segments (e.g., W10=). Since isBase64(..., { urlSafe: true }) rejects =, these cases fail before exercising the new base64url normalization + JSON parsing logic. If the intent is to test “non-object decoded values”, use the unpadded base64url forms (e.g., W10 for []) so the decode/parse path is actually covered.
| 'W10=.eyJiYXIiOiJiYXoifQ.', | |
| 'eyJmb28iOiJiYXIifQ.W10=.', | |
| 'W10.eyJiYXIiOiJiYXoifQ.', | |
| 'eyJmb28iOiJiYXIifQ.W10.', |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@rubiin @profnandaa @WikiRik please check |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Small follow-up for context: Standards-compliant JWTs continue to pass unchanged; only malformed tokens previously accepted are now rejected. |
Fixes #2511
This PR updates
isJWT()to perform structural validation beyond checking Base64URL segments.What changed
-→+,_→/)alg: none)Why
The previous implementation accepted any three Base64URL-looking segments as valid JWTs, including invalid examples such as:
foo.bar....t.These pass Base64 checks but fail RFC 7519 https://datatracker.ietf.org/doc/html/rfc7519 structural requirements because header and payload must decode to valid JSON objects.
Reference
RFC 7519 Section 7.2 requires:
Tests added
Checklist