Skip to content

BUG(isJWT): validate decoded header and payload as JSON objects#2677

Open
Kartikeya-guthub wants to merge 11 commits intovalidatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation
Open

BUG(isJWT): validate decoded header and payload as JSON objects#2677
Kartikeya-guthub wants to merge 11 commits intovalidatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub
Copy link
Contributor

@Kartikeya-guthub Kartikeya-guthub commented Mar 7, 2026

Fixes #2511

This PR updates isJWT() to perform structural validation beyond checking Base64URL segments.

What changed

  • Decode header and payload from Base64URL
  • Normalize URL-safe Base64 characters (-+, _/)
  • Restore missing padding before decoding
  • Parse decoded values as JSON
  • Ensure both header and payload are valid JSON objects
  • Preserve empty signature support for unsecured JWTs (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:

  • JOSE Header must decode to a valid JSON object
  • JWT Claims Set must decode to a valid JSON object

Tests added

  • invalid decoded header JSON
  • invalid decoded payload JSON
  • non-object decoded values
  • unsecured JWT with empty signature

Checklist

  • PR contains only changes related; no stray files, etc.
  • README updated (where applicable)
  • Tests written (where applicable)
  • References provided in PR (where applicable)

@codecov
Copy link

codecov bot commented Mar 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b1aea75) to head (6aae9bc).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.parse them to ensure they are JSON objects (not arrays/null/primitives).
  • Keep allowing an empty signature segment (trailing .) for unsecured JWTs.
  • Extend the isJWT invalid 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
Comment on lines +4 to +10
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');
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +5552 to +5556
'foo.bar.',
'..',
'.t.',
'foo.bar.baz',
'Zm9v.YmFy.',
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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');
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
src/lib/isJWT.js Outdated
Comment on lines +4 to +6
/* istanbul ignore next */
function decodeBase64Url(b64) {
if (typeof Buffer !== 'undefined') {
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/* 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.

Copilot uses AI. Check for mistakes.
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',
'W10.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10.',

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Kartikeya-guthub
Copy link
Contributor Author

Kartikeya-guthub commented Mar 7, 2026

@rubiin @profnandaa @WikiRik please check

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Kartikeya-guthub Kartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objects BUG(isJWT): validate decoded header and payload as JSON objects Mar 9, 2026
@Kartikeya-guthub
Copy link
Contributor Author

Small follow-up for context: isJWT() has previously been tightened to reject structurally invalid tokens (for example, enforcing exactly 3 segments in #2217). This change follows the same direction by rejecting tokens whose header/payload are base64url-valid but do not decode to JSON objects as required by RFC 7519.

Standards-compliant JWTs continue to pass unchanged; only malformed tokens previously accepted are now rejected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isJWT does not check whether the decoded sections are valid JSON

2 participants