diff --git a/src/lib/isJWT.js b/src/lib/isJWT.js index 1d0ade5ee..9bb422a86 100644 --- a/src/lib/isJWT.js +++ b/src/lib/isJWT.js @@ -1,15 +1,70 @@ import assertString from './util/assertString'; import isBase64 from './isBase64'; +function decodeBase64Url(b64) { + /* istanbul ignore else */ + if (typeof Buffer !== 'undefined') { + /* istanbul ignore else */ + if (typeof Buffer.from === 'function') { + return Buffer.from(b64, 'base64').toString('utf8'); + } + /* istanbul ignore next */ + // eslint-disable-next-line no-buffer-constructor + return new Buffer(b64, 'base64').toString('utf8'); + } + /* istanbul ignore next */ + if (typeof atob === 'function') { + const binary = atob(b64); + if (typeof TextDecoder !== 'undefined') { + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) { + bytes[i] = binary.charCodeAt(i); + } + return new TextDecoder('utf-8').decode(bytes); + } + let encoded = ''; + for (let i = 0; i < binary.length; i += 1) { + const hex = binary.charCodeAt(i).toString(16); + const code = hex.length === 1 ? `0${hex}` : hex; + encoded += `%${code}`; + } + return decodeURIComponent(encoded); + } + /* istanbul ignore next */ + return b64; +} + +function tryDecodeJSON(segment) { + if (!isBase64(segment, { urlSafe: true })) return false; + try { + // Normalize base64url alphabet to base64, then restore stripped padding + let b64 = segment.replace(/-/g, '+').replace(/_/g, '/'); + while (b64.length % 4) b64 += '='; + const decoded = decodeBase64Url(b64); + const parsed = JSON.parse(decoded); + if (typeof parsed !== 'object') return false; + if (parsed === null) return false; + if (Array.isArray(parsed)) return false; + return parsed; + } catch (e) { + return false; + } +} + export default function isJWT(str) { assertString(str); const dotSplit = str.split('.'); - const len = dotSplit.length; - if (len !== 3) { - return false; - } + if (dotSplit.length !== 3) return false; + + const header = dotSplit[0]; + const payload = dotSplit[1]; + const signature = dotSplit[2]; + + if (!tryDecodeJSON(header)) return false; + if (!tryDecodeJSON(payload)) return false; + if (!isBase64(signature, { urlSafe: true })) return false; - return dotSplit.reduce((acc, currElem) => acc && isBase64(currElem, { urlSafe: true }), true); + return true; } diff --git a/test/validators.test.js b/test/validators.test.js index 010d4fa5a..2b50c7a1d 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -5542,6 +5542,7 @@ describe('Validators', () => { 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb3JlbSI6Imlwc3VtIn0.ymiJSsMJXR6tMSr8G9usjQ15_8hKPDv_CArLhxw28MI', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkb2xvciI6InNpdCIsImFtZXQiOlsibG9yZW0iLCJpcHN1bSJdfQ.rRpe04zbWbbJjwM43VnHzAboDzszJtGrNsUxaqQ-GQ8', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqb2huIjp7ImFnZSI6MjUsImhlaWdodCI6MTg1fSwiamFrZSI6eyJhZ2UiOjMwLCJoZWlnaHQiOjI3MH19.YRLPARDmhGMC3BBk_OhtwwK21PIkVCqQe8ncIRPKo-E', + 'eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.', ], invalid: [ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', @@ -5549,6 +5550,19 @@ describe('Validators', () => { 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTYxNjY1Mzg3Mn0.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiaWF0IjoxNjE2NjUzODcyLCJleHAiOjE2MTY2NTM4ODJ9.a1jLRQkO5TV5y5ERcaPAiM9Xm2gBdRjKrrCpHkGr_8M', '$Zs.ewu.su84', 'ks64$S/9.dy$§kz.3sd73b', + 'foo.bar.', + '..', + '.t.', + 'foo.bar.baz', + 'Zm9v.YmFy.', + 'eyJmb28iOiJiYXIifQ.YmFy.', + 'Zm9v.eyJiYXIiOiJiYXoifQ.', + 'W10.eyJiYXIiOiJiYXoifQ.', + 'eyJmb28iOiJiYXIifQ.W10.', + 'bnVsbA.eyJiYXIiOiJiYXoifQ.', + 'WzFd.eyJiYXIiOiJiYXoifQ.', + 'ImhlbGxvIg.eyJiYXIiOiJiYXoifQ.', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.invalid$sig', ], error: [ [],