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

Open
Kartikeya-guthub wants to merge 11 commits into
validatorjs: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 into
validatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub

@Kartikeya-guthubKartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

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

codecovBot commented Mar 7, 2026

Copy link
Copy Markdown

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/lib/isJWT.jsAdds base64url decode + JSON-object structural validation for header/payload.
test/validators.test.jsAdds additional invalid JWT strings to cover new structural validation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=Buffer.from(b64,'base64').toString('utf8');
functiondecodeBase64UrlToUtf8(b64){
// Prefer Node.js Buffer when available (supports old Node via `new Buffer`).
if(typeofBuffer!=='undefined'){
if(typeofBuffer.from==='function'){
returnBuffer.from(b64,'base64').toString('utf8');
}
// Fallback for very old Node versions where Buffer.from is not available.
// eslint-disable-next-line no-buffer-constructor
returnnewBuffer(b64,'base64').toString('utf8');
}
// Browser / non-Node environment: use atob/TextDecoder if available.
if(typeofatob==='function'){
constbinary=atob(b64);
// If TextDecoder is available, use it for proper UTF-8 decoding.
if(typeofTextDecoder!=='undefined'){
constbytes=newUint8Array(binary.length);
for(leti=0;i<binary.length;i+=1){
bytes[i]=binary.charCodeAt(i);
}
returnnewTextDecoder('utf-8').decode(bytes);
}
// Fallback UTF-8 decoding using percent-encoding.
letencoded='';
for(leti=0;i<binary.length;i+=1){
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
encoded+='%'+code;
}
returndecodeURIComponent(encoded);
}
// As a last resort, return the input unchanged.
returnb64;
}
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=decodeBase64UrlToUtf8(b64);

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

CopilotAIMar 7, 2026

Copy link

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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
consthex=binary.charCodeAt(i).toString(16);
constcode=hex.length===1 ? `0${hex}` : hex;

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

CopilotAIMar 7, 2026

Copy link

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 threadtest/validators.test.js Outdated
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',

CopilotAIMar 7, 2026

Copy link

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

Kartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
ContributorAuthor

@rubiin@profnandaa@WikiRik please check

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js
Comment threadsrc/lib/isJWT.js
@Kartikeya-guthubKartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objectsBUG(isJWT): validate decoded header and payload as JSON objectsMar 9, 2026
@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

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.

@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

All review feedback has been addressed and CI is green now ✅
Would appreciate a final maintainer review when available. Thanks 🙏
@WikiRik@profnandaa@rubiin

Sign up for freeto 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

@Kartikeya-guthub
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Open
Kartikeya-guthub wants to merge 11 commits into
validatorjs: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 into
validatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub

@Kartikeya-guthubKartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

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

codecovBot commented Mar 7, 2026

Copy link
Copy Markdown

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/lib/isJWT.jsAdds base64url decode + JSON-object structural validation for header/payload.
test/validators.test.jsAdds additional invalid JWT strings to cover new structural validation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=Buffer.from(b64,'base64').toString('utf8');
functiondecodeBase64UrlToUtf8(b64){
// Prefer Node.js Buffer when available (supports old Node via `new Buffer`).
if(typeofBuffer!=='undefined'){
if(typeofBuffer.from==='function'){
returnBuffer.from(b64,'base64').toString('utf8');
}
// Fallback for very old Node versions where Buffer.from is not available.
// eslint-disable-next-line no-buffer-constructor
returnnewBuffer(b64,'base64').toString('utf8');
}
// Browser / non-Node environment: use atob/TextDecoder if available.
if(typeofatob==='function'){
constbinary=atob(b64);
// If TextDecoder is available, use it for proper UTF-8 decoding.
if(typeofTextDecoder!=='undefined'){
constbytes=newUint8Array(binary.length);
for(leti=0;i<binary.length;i+=1){
bytes[i]=binary.charCodeAt(i);
}
returnnewTextDecoder('utf-8').decode(bytes);
}
// Fallback UTF-8 decoding using percent-encoding.
letencoded='';
for(leti=0;i<binary.length;i+=1){
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
encoded+='%'+code;
}
returndecodeURIComponent(encoded);
}
// As a last resort, return the input unchanged.
returnb64;
}
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=decodeBase64UrlToUtf8(b64);

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

CopilotAIMar 7, 2026

Copy link

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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
consthex=binary.charCodeAt(i).toString(16);
constcode=hex.length===1 ? `0${hex}` : hex;

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

CopilotAIMar 7, 2026

Copy link

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 threadtest/validators.test.js Outdated
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',

CopilotAIMar 7, 2026

Copy link

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

Kartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
ContributorAuthor

@rubiin@profnandaa@WikiRik please check

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js
Comment threadsrc/lib/isJWT.js
@Kartikeya-guthubKartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objectsBUG(isJWT): validate decoded header and payload as JSON objectsMar 9, 2026
@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

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.

@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

All review feedback has been addressed and CI is green now ✅
Would appreciate a final maintainer review when available. Thanks 🙏
@WikiRik@profnandaa@rubiin

Sign up for freeto 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

@Kartikeya-guthub
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Open
Kartikeya-guthub wants to merge 11 commits into
validatorjs: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 into
validatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub

@Kartikeya-guthubKartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

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

codecovBot commented Mar 7, 2026

Copy link
Copy Markdown

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/lib/isJWT.jsAdds base64url decode + JSON-object structural validation for header/payload.
test/validators.test.jsAdds additional invalid JWT strings to cover new structural validation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=Buffer.from(b64,'base64').toString('utf8');
functiondecodeBase64UrlToUtf8(b64){
// Prefer Node.js Buffer when available (supports old Node via `new Buffer`).
if(typeofBuffer!=='undefined'){
if(typeofBuffer.from==='function'){
returnBuffer.from(b64,'base64').toString('utf8');
}
// Fallback for very old Node versions where Buffer.from is not available.
// eslint-disable-next-line no-buffer-constructor
returnnewBuffer(b64,'base64').toString('utf8');
}
// Browser / non-Node environment: use atob/TextDecoder if available.
if(typeofatob==='function'){
constbinary=atob(b64);
// If TextDecoder is available, use it for proper UTF-8 decoding.
if(typeofTextDecoder!=='undefined'){
constbytes=newUint8Array(binary.length);
for(leti=0;i<binary.length;i+=1){
bytes[i]=binary.charCodeAt(i);
}
returnnewTextDecoder('utf-8').decode(bytes);
}
// Fallback UTF-8 decoding using percent-encoding.
letencoded='';
for(leti=0;i<binary.length;i+=1){
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
encoded+='%'+code;
}
returndecodeURIComponent(encoded);
}
// As a last resort, return the input unchanged.
returnb64;
}
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=decodeBase64UrlToUtf8(b64);

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

CopilotAIMar 7, 2026

Copy link

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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
consthex=binary.charCodeAt(i).toString(16);
constcode=hex.length===1 ? `0${hex}` : hex;

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

CopilotAIMar 7, 2026

Copy link

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 threadtest/validators.test.js Outdated
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',

CopilotAIMar 7, 2026

Copy link

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

Kartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
ContributorAuthor

@rubiin@profnandaa@WikiRik please check

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js
Comment threadsrc/lib/isJWT.js
@Kartikeya-guthubKartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objectsBUG(isJWT): validate decoded header and payload as JSON objectsMar 9, 2026
@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

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.

@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

All review feedback has been addressed and CI is green now ✅
Would appreciate a final maintainer review when available. Thanks 🙏
@WikiRik@profnandaa@rubiin

Sign up for freeto 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

@Kartikeya-guthub
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Open
Kartikeya-guthub wants to merge 11 commits into
validatorjs: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 into
validatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub

@Kartikeya-guthubKartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

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

codecovBot commented Mar 7, 2026

Copy link
Copy Markdown

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/lib/isJWT.jsAdds base64url decode + JSON-object structural validation for header/payload.
test/validators.test.jsAdds additional invalid JWT strings to cover new structural validation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=Buffer.from(b64,'base64').toString('utf8');
functiondecodeBase64UrlToUtf8(b64){
// Prefer Node.js Buffer when available (supports old Node via `new Buffer`).
if(typeofBuffer!=='undefined'){
if(typeofBuffer.from==='function'){
returnBuffer.from(b64,'base64').toString('utf8');
}
// Fallback for very old Node versions where Buffer.from is not available.
// eslint-disable-next-line no-buffer-constructor
returnnewBuffer(b64,'base64').toString('utf8');
}
// Browser / non-Node environment: use atob/TextDecoder if available.
if(typeofatob==='function'){
constbinary=atob(b64);
// If TextDecoder is available, use it for proper UTF-8 decoding.
if(typeofTextDecoder!=='undefined'){
constbytes=newUint8Array(binary.length);
for(leti=0;i<binary.length;i+=1){
bytes[i]=binary.charCodeAt(i);
}
returnnewTextDecoder('utf-8').decode(bytes);
}
// Fallback UTF-8 decoding using percent-encoding.
letencoded='';
for(leti=0;i<binary.length;i+=1){
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
encoded+='%'+code;
}
returndecodeURIComponent(encoded);
}
// As a last resort, return the input unchanged.
returnb64;
}
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=decodeBase64UrlToUtf8(b64);

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

CopilotAIMar 7, 2026

Copy link

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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
consthex=binary.charCodeAt(i).toString(16);
constcode=hex.length===1 ? `0${hex}` : hex;

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

CopilotAIMar 7, 2026

Copy link

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 threadtest/validators.test.js Outdated
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',

CopilotAIMar 7, 2026

Copy link

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

Kartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
ContributorAuthor

@rubiin@profnandaa@WikiRik please check

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js
Comment threadsrc/lib/isJWT.js
@Kartikeya-guthubKartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objectsBUG(isJWT): validate decoded header and payload as JSON objectsMar 9, 2026
@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

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.

@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

All review feedback has been addressed and CI is green now ✅
Would appreciate a final maintainer review when available. Thanks 🙏
@WikiRik@profnandaa@rubiin

Sign up for freeto 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

@Kartikeya-guthub
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Open
Kartikeya-guthub wants to merge 11 commits into
validatorjs: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 into
validatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub

@Kartikeya-guthubKartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

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

codecovBot commented Mar 7, 2026

Copy link
Copy Markdown

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/lib/isJWT.jsAdds base64url decode + JSON-object structural validation for header/payload.
test/validators.test.jsAdds additional invalid JWT strings to cover new structural validation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=Buffer.from(b64,'base64').toString('utf8');
functiondecodeBase64UrlToUtf8(b64){
// Prefer Node.js Buffer when available (supports old Node via `new Buffer`).
if(typeofBuffer!=='undefined'){
if(typeofBuffer.from==='function'){
returnBuffer.from(b64,'base64').toString('utf8');
}
// Fallback for very old Node versions where Buffer.from is not available.
// eslint-disable-next-line no-buffer-constructor
returnnewBuffer(b64,'base64').toString('utf8');
}
// Browser / non-Node environment: use atob/TextDecoder if available.
if(typeofatob==='function'){
constbinary=atob(b64);
// If TextDecoder is available, use it for proper UTF-8 decoding.
if(typeofTextDecoder!=='undefined'){
constbytes=newUint8Array(binary.length);
for(leti=0;i<binary.length;i+=1){
bytes[i]=binary.charCodeAt(i);
}
returnnewTextDecoder('utf-8').decode(bytes);
}
// Fallback UTF-8 decoding using percent-encoding.
letencoded='';
for(leti=0;i<binary.length;i+=1){
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
encoded+='%'+code;
}
returndecodeURIComponent(encoded);
}
// As a last resort, return the input unchanged.
returnb64;
}
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=decodeBase64UrlToUtf8(b64);

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

CopilotAIMar 7, 2026

Copy link

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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
consthex=binary.charCodeAt(i).toString(16);
constcode=hex.length===1 ? `0${hex}` : hex;

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

CopilotAIMar 7, 2026

Copy link

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 threadtest/validators.test.js Outdated
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',

CopilotAIMar 7, 2026

Copy link

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

Kartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
ContributorAuthor

@rubiin@profnandaa@WikiRik please check

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js
Comment threadsrc/lib/isJWT.js
@Kartikeya-guthubKartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objectsBUG(isJWT): validate decoded header and payload as JSON objectsMar 9, 2026
@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

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.

@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

All review feedback has been addressed and CI is green now ✅
Would appreciate a final maintainer review when available. Thanks 🙏
@WikiRik@profnandaa@rubiin

Sign up for freeto 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

@Kartikeya-guthub
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Open
Kartikeya-guthub wants to merge 11 commits into
validatorjs: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 into
validatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub

@Kartikeya-guthubKartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

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

codecovBot commented Mar 7, 2026

Copy link
Copy Markdown

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/lib/isJWT.jsAdds base64url decode + JSON-object structural validation for header/payload.
test/validators.test.jsAdds additional invalid JWT strings to cover new structural validation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=Buffer.from(b64,'base64').toString('utf8');
functiondecodeBase64UrlToUtf8(b64){
// Prefer Node.js Buffer when available (supports old Node via `new Buffer`).
if(typeofBuffer!=='undefined'){
if(typeofBuffer.from==='function'){
returnBuffer.from(b64,'base64').toString('utf8');
}
// Fallback for very old Node versions where Buffer.from is not available.
// eslint-disable-next-line no-buffer-constructor
returnnewBuffer(b64,'base64').toString('utf8');
}
// Browser / non-Node environment: use atob/TextDecoder if available.
if(typeofatob==='function'){
constbinary=atob(b64);
// If TextDecoder is available, use it for proper UTF-8 decoding.
if(typeofTextDecoder!=='undefined'){
constbytes=newUint8Array(binary.length);
for(leti=0;i<binary.length;i+=1){
bytes[i]=binary.charCodeAt(i);
}
returnnewTextDecoder('utf-8').decode(bytes);
}
// Fallback UTF-8 decoding using percent-encoding.
letencoded='';
for(leti=0;i<binary.length;i+=1){
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
encoded+='%'+code;
}
returndecodeURIComponent(encoded);
}
// As a last resort, return the input unchanged.
returnb64;
}
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=decodeBase64UrlToUtf8(b64);

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

CopilotAIMar 7, 2026

Copy link

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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
consthex=binary.charCodeAt(i).toString(16);
constcode=hex.length===1 ? `0${hex}` : hex;

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

CopilotAIMar 7, 2026

Copy link

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 threadtest/validators.test.js Outdated
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',

CopilotAIMar 7, 2026

Copy link

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

Kartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
ContributorAuthor

@rubiin@profnandaa@WikiRik please check

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js
Comment threadsrc/lib/isJWT.js
@Kartikeya-guthubKartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objectsBUG(isJWT): validate decoded header and payload as JSON objectsMar 9, 2026
@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

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.

@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

All review feedback has been addressed and CI is green now ✅
Would appreciate a final maintainer review when available. Thanks 🙏
@WikiRik@profnandaa@rubiin

Sign up for freeto 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

@Kartikeya-guthub
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Open
Kartikeya-guthub wants to merge 11 commits into
validatorjs: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 into
validatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub

@Kartikeya-guthubKartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

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

codecovBot commented Mar 7, 2026

Copy link
Copy Markdown

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/lib/isJWT.jsAdds base64url decode + JSON-object structural validation for header/payload.
test/validators.test.jsAdds additional invalid JWT strings to cover new structural validation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=Buffer.from(b64,'base64').toString('utf8');
functiondecodeBase64UrlToUtf8(b64){
// Prefer Node.js Buffer when available (supports old Node via `new Buffer`).
if(typeofBuffer!=='undefined'){
if(typeofBuffer.from==='function'){
returnBuffer.from(b64,'base64').toString('utf8');
}
// Fallback for very old Node versions where Buffer.from is not available.
// eslint-disable-next-line no-buffer-constructor
returnnewBuffer(b64,'base64').toString('utf8');
}
// Browser / non-Node environment: use atob/TextDecoder if available.
if(typeofatob==='function'){
constbinary=atob(b64);
// If TextDecoder is available, use it for proper UTF-8 decoding.
if(typeofTextDecoder!=='undefined'){
constbytes=newUint8Array(binary.length);
for(leti=0;i<binary.length;i+=1){
bytes[i]=binary.charCodeAt(i);
}
returnnewTextDecoder('utf-8').decode(bytes);
}
// Fallback UTF-8 decoding using percent-encoding.
letencoded='';
for(leti=0;i<binary.length;i+=1){
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
encoded+='%'+code;
}
returndecodeURIComponent(encoded);
}
// As a last resort, return the input unchanged.
returnb64;
}
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=decodeBase64UrlToUtf8(b64);

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

CopilotAIMar 7, 2026

Copy link

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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
consthex=binary.charCodeAt(i).toString(16);
constcode=hex.length===1 ? `0${hex}` : hex;

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

CopilotAIMar 7, 2026

Copy link

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 threadtest/validators.test.js Outdated
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',

CopilotAIMar 7, 2026

Copy link

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

Kartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
ContributorAuthor

@rubiin@profnandaa@WikiRik please check

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js
Comment threadsrc/lib/isJWT.js
@Kartikeya-guthubKartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objectsBUG(isJWT): validate decoded header and payload as JSON objectsMar 9, 2026
@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

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.

@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

All review feedback has been addressed and CI is green now ✅
Would appreciate a final maintainer review when available. Thanks 🙏
@WikiRik@profnandaa@rubiin

Sign up for freeto 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

@Kartikeya-guthub
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Open
Kartikeya-guthub wants to merge 11 commits into
validatorjs: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 into
validatorjs:masterfrom
Kartikeya-guthub:fix/isJWT-json-validation

Conversation

@Kartikeya-guthub

@Kartikeya-guthubKartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

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

codecovBot commented Mar 7, 2026

Copy link
Copy Markdown

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/lib/isJWT.jsAdds base64url decode + JSON-object structural validation for header/payload.
test/validators.test.jsAdds additional invalid JWT strings to cover new structural validation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=Buffer.from(b64,'base64').toString('utf8');
functiondecodeBase64UrlToUtf8(b64){
// Prefer Node.js Buffer when available (supports old Node via `new Buffer`).
if(typeofBuffer!=='undefined'){
if(typeofBuffer.from==='function'){
returnBuffer.from(b64,'base64').toString('utf8');
}
// Fallback for very old Node versions where Buffer.from is not available.
// eslint-disable-next-line no-buffer-constructor
returnnewBuffer(b64,'base64').toString('utf8');
}
// Browser / non-Node environment: use atob/TextDecoder if available.
if(typeofatob==='function'){
constbinary=atob(b64);
// If TextDecoder is available, use it for proper UTF-8 decoding.
if(typeofTextDecoder!=='undefined'){
constbytes=newUint8Array(binary.length);
for(leti=0;i<binary.length;i+=1){
bytes[i]=binary.charCodeAt(i);
}
returnnewTextDecoder('utf-8').decode(bytes);
}
// Fallback UTF-8 decoding using percent-encoding.
letencoded='';
for(leti=0;i<binary.length;i+=1){
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
encoded+='%'+code;
}
returndecodeURIComponent(encoded);
}
// As a last resort, return the input unchanged.
returnb64;
}
functiontryDecodeJSON(segment){
if(!isBase64(segment,{urlSafe: true}))returnfalse;
try{
// Normalize base64url alphabet to base64, then restore stripped padding
letb64=segment.replace(/-/g,'+').replace(/_/g,'/');
while(b64.length%4)b64+='=';
constdecoded=decodeBase64UrlToUtf8(b64);

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

CopilotAIMar 7, 2026

Copy link

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
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/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');

CopilotAIMar 7, 2026

Copy link

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
constcode=binary.charCodeAt(i).toString(16).padStart(2,'0');
consthex=binary.charCodeAt(i).toString(16);
constcode=hex.length===1 ? `0${hex}` : hex;

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

CopilotAIMar 7, 2026

Copy link

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 threadtest/validators.test.js Outdated
Comment on lines +5560 to +5561
'W10=.eyJiYXIiOiJiYXoifQ.',
'eyJmb28iOiJiYXIifQ.W10=.',

CopilotAIMar 7, 2026

Copy link

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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

Kartikeya-guthub commented Mar 7, 2026

Copy link
Copy Markdown
ContributorAuthor

@rubiin@profnandaa@WikiRik please check

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/lib/isJWT.js
Comment threadsrc/lib/isJWT.js
@Kartikeya-guthubKartikeya-guthub changed the title fix(isJWT): validate decoded header and payload as JSON objectsBUG(isJWT): validate decoded header and payload as JSON objectsMar 9, 2026
@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

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.

@Kartikeya-guthub

Copy link
Copy Markdown
ContributorAuthor

All review feedback has been addressed and CI is green now ✅
Would appreciate a final maintainer review when available. Thanks 🙏
@WikiRik@profnandaa@rubiin

Sign up for freeto 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

@Kartikeya-guthub