eslint-plugin-jwt-security 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/AGENTS.md +102 -0
  2. package/CHANGELOG.md +161 -0
  3. package/LICENSE +23 -0
  4. package/README.md +158 -0
  5. package/dist/AGENTS.md +102 -0
  6. package/dist/CHANGELOG.md +161 -0
  7. package/dist/LICENSE +23 -0
  8. package/dist/README.md +158 -0
  9. package/dist/package.json +125 -0
  10. package/dist/src/index.d.ts +101 -0
  11. package/dist/src/index.js +171 -0
  12. package/dist/src/oxlint.d.ts +20 -0
  13. package/dist/src/oxlint.js +21 -0
  14. package/dist/src/rules/no-algorithm-confusion/index.d.ts +24 -0
  15. package/dist/src/rules/no-algorithm-confusion/index.js +165 -0
  16. package/dist/src/rules/no-algorithm-none/index.d.ts +24 -0
  17. package/dist/src/rules/no-algorithm-none/index.js +222 -0
  18. package/dist/src/rules/no-decode-without-verify/index.d.ts +23 -0
  19. package/dist/src/rules/no-decode-without-verify/index.js +131 -0
  20. package/dist/src/rules/no-hardcoded-secret/index.d.ts +23 -0
  21. package/dist/src/rules/no-hardcoded-secret/index.js +189 -0
  22. package/dist/src/rules/no-sensitive-payload/index.d.ts +20 -0
  23. package/dist/src/rules/no-sensitive-payload/index.js +103 -0
  24. package/dist/src/rules/no-timestamp-manipulation/index.d.ts +23 -0
  25. package/dist/src/rules/no-timestamp-manipulation/index.js +97 -0
  26. package/dist/src/rules/no-weak-secret/index.d.ts +23 -0
  27. package/dist/src/rules/no-weak-secret/index.js +165 -0
  28. package/dist/src/rules/require-algorithm-whitelist/index.d.ts +24 -0
  29. package/dist/src/rules/require-algorithm-whitelist/index.js +113 -0
  30. package/dist/src/rules/require-audience-validation/index.d.ts +20 -0
  31. package/dist/src/rules/require-audience-validation/index.js +100 -0
  32. package/dist/src/rules/require-expiration/index.d.ts +23 -0
  33. package/dist/src/rules/require-expiration/index.js +136 -0
  34. package/dist/src/rules/require-issued-at/index.d.ts +23 -0
  35. package/dist/src/rules/require-issued-at/index.js +113 -0
  36. package/dist/src/rules/require-issuer-validation/index.d.ts +20 -0
  37. package/dist/src/rules/require-issuer-validation/index.js +100 -0
  38. package/dist/src/rules/require-max-age/index.d.ts +24 -0
  39. package/dist/src/rules/require-max-age/index.js +93 -0
  40. package/dist/src/types/index.d.ts +82 -0
  41. package/dist/src/types/index.js +7 -0
  42. package/dist/src/utils/index.d.ts +98 -0
  43. package/dist/src/utils/index.js +238 -0
  44. package/package.json +125 -0
  45. package/src/__compatibility__/jwt-interface.spec.ts +192 -0
  46. package/src/index.test.ts +47 -0
  47. package/src/index.ts +209 -0
  48. package/src/oxlint.test.ts +61 -0
  49. package/src/oxlint.ts +23 -0
  50. package/src/rules/no-algorithm-confusion/index.ts +208 -0
  51. package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +106 -0
  52. package/src/rules/no-algorithm-none/index.ts +270 -0
  53. package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +146 -0
  54. package/src/rules/no-decode-without-verify/index.ts +162 -0
  55. package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +76 -0
  56. package/src/rules/no-hardcoded-secret/index.ts +230 -0
  57. package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +97 -0
  58. package/src/rules/no-sensitive-payload/index.ts +123 -0
  59. package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +157 -0
  60. package/src/rules/no-timestamp-manipulation/index.ts +118 -0
  61. package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +54 -0
  62. package/src/rules/no-weak-secret/index.ts +204 -0
  63. package/src/rules/no-weak-secret/no-weak-secret.test.ts +114 -0
  64. package/src/rules/require-algorithm-whitelist/index.ts +145 -0
  65. package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +72 -0
  66. package/src/rules/require-audience-validation/index.ts +120 -0
  67. package/src/rules/require-audience-validation/require-audience-validation.test.ts +68 -0
  68. package/src/rules/require-expiration/index.ts +170 -0
  69. package/src/rules/require-expiration/require-expiration.test.ts +135 -0
  70. package/src/rules/require-issued-at/index.ts +143 -0
  71. package/src/rules/require-issued-at/require-issued-at.test.ts +64 -0
  72. package/src/rules/require-issuer-validation/index.ts +120 -0
  73. package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +68 -0
  74. package/src/rules/require-max-age/index.ts +117 -0
  75. package/src/rules/require-max-age/require-max-age.test.ts +69 -0
  76. package/src/tests/coverage-dual-layer.test.ts +323 -0
  77. package/src/types/index.ts +107 -0
  78. package/src/utils/index.ts +285 -0
  79. package/src/utils/utils.test.ts +324 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Copyright (c) 2025 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+
7
+ /**
8
+ * ESLint Rule: no-algorithm-none
9
+ *
10
+ * Detects attempts to use the 'none' algorithm which bypasses JWT signature verification.
11
+ * This is a critical vulnerability (CVE-2022-23540) that allows attackers to forge JWT tokens.
12
+ *
13
+ * CWE-347: Improper Verification of Cryptographic Signature
14
+ *
15
+ * @see https://nvd.nist.gov/vuln/detail/CVE-2022-23540
16
+ * @see https://tools.ietf.org/html/rfc8725
17
+ */
18
+ import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
19
+ import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
20
+ import {
21
+ isVerifyOperation,
22
+ isSignOperation,
23
+ getOptionsArgument,
24
+ isTestFile,
25
+ } from '../../utils';
26
+ import type { NoAlgorithmNoneOptions } from '../../types';
27
+
28
+ type MessageIds =
29
+ | 'algorithmNone'
30
+ | 'algorithmNoneInArray'
31
+ | 'emptyAlgorithms'
32
+ | 'useSecureAlgorithm'
33
+ | 'decodeWithoutVerify';
34
+
35
+ type RuleOptions = [NoAlgorithmNoneOptions?];
36
+
37
+ export const noAlgorithmNone = createRule<RuleOptions, MessageIds>({
38
+ name: 'no-algorithm-none',
39
+ meta: {
40
+ type: 'problem',
41
+ docs: {
42
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-algorithm-none.md',
43
+ description:
44
+ 'Disallow JWT "none" algorithm which bypasses signature verification (CVE-2022-23540)',
45
+ cwe: 'CWE-347',
46
+ cvss: 9.5,
47
+ confidence: 'high',
48
+ },
49
+ fixable: undefined,
50
+ hasSuggestions: false,
51
+ messages: {
52
+ algorithmNone: formatLLMMessage({
53
+ icon: MessageIcons.SECURITY,
54
+ issueName: 'JWT Algorithm None Attack',
55
+ cwe: 'CWE-347',
56
+ description:
57
+ 'Using alg:"none" bypasses signature verification, allowing token forgery',
58
+ severity: 'CRITICAL',
59
+ fix: 'Remove "none" and use RS256, ES256, or other secure algorithms',
60
+ documentationLink: 'https://nvd.nist.gov/vuln/detail/CVE-2022-23540',
61
+ }),
62
+ algorithmNoneInArray: formatLLMMessage({
63
+ icon: MessageIcons.SECURITY,
64
+ issueName: 'JWT Algorithm None in Whitelist',
65
+ cwe: 'CWE-347',
66
+ description:
67
+ 'Including "none" in algorithms array allows unsigned tokens',
68
+ severity: 'CRITICAL',
69
+ fix: 'Remove "none" from the algorithms array',
70
+ documentationLink: 'https://nvd.nist.gov/vuln/detail/CVE-2022-23540',
71
+ }),
72
+ emptyAlgorithms: formatLLMMessage({
73
+ icon: MessageIcons.SECURITY,
74
+ issueName: 'Empty Algorithms Array',
75
+ cwe: 'CWE-347',
76
+ description:
77
+ 'Empty algorithms array may default to accepting any algorithm including none',
78
+ severity: 'HIGH',
79
+ fix: 'Specify explicit algorithms: ["RS256"] or ["ES256"]',
80
+ documentationLink: 'https://tools.ietf.org/html/rfc8725',
81
+ }),
82
+ useSecureAlgorithm: formatLLMMessage({
83
+ icon: MessageIcons.INFO,
84
+ issueName: 'Use Secure Algorithm',
85
+ description: 'Replace with a secure algorithm',
86
+ severity: 'LOW',
87
+ fix: 'Use algorithms: ["RS256"] or algorithms: ["ES256"]',
88
+ documentationLink: 'https://tools.ietf.org/html/rfc8725',
89
+ }),
90
+ decodeWithoutVerify: formatLLMMessage({
91
+ icon: MessageIcons.SECURITY,
92
+ issueName: 'JWT Decode Without Verify',
93
+ cwe: 'CWE-347',
94
+ description:
95
+ 'jwt.decode() does NOT verify the signature — using its result as if it were authenticated is functionally equivalent to algorithm:"none". Any token, including forged ones, will decode.',
96
+ severity: 'CRITICAL',
97
+ fix: 'Use jwt.verify(token, secret, { algorithms: ["RS256"] }) instead. Reserve jwt.decode() for non-security inspection only (e.g. logging the issuer).',
98
+ documentationLink: 'https://github.com/auth0/node-jsonwebtoken#jwtdecodetoken--options',
99
+ }),
100
+ },
101
+ schema: [
102
+ {
103
+ type: 'object',
104
+ properties: {
105
+ allowInTests: {
106
+ type: 'boolean',
107
+ default: false,
108
+ description: 'Allow "none" algorithm in test files',
109
+ },
110
+ trustedSanitizers: {
111
+ type: 'array',
112
+ items: { type: 'string' },
113
+ default: [],
114
+ },
115
+ trustedAnnotations: {
116
+ type: 'array',
117
+ items: { type: 'string' },
118
+ default: [],
119
+ },
120
+ strictMode: {
121
+ type: 'boolean',
122
+ default: false,
123
+ },
124
+ },
125
+ additionalProperties: false,
126
+ },
127
+ ],
128
+ },
129
+ defaultOptions: [
130
+ {
131
+ allowInTests: false,
132
+ trustedSanitizers: [],
133
+ trustedAnnotations: [],
134
+ strictMode: false,
135
+ },
136
+ ],
137
+ create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
138
+ const options = context.options[0] ?? {};
139
+ const { allowInTests = false } = options;
140
+ const filename = context.filename;
141
+
142
+ // Skip test files if configured
143
+ if (allowInTests && isTestFile(filename)) {
144
+ return {};
145
+ }
146
+
147
+ /**
148
+ * Check if an algorithm value is 'none' (case insensitive)
149
+ */
150
+ // oxlint-disable-next-line consistent-function-scoping
151
+ const isNoneAlgorithm = (value: string): boolean => {
152
+ return value.toLowerCase() === 'none';
153
+ };
154
+
155
+ /**
156
+ * Check options object for 'none' algorithm
157
+ */
158
+ const checkOptionsForNone = (
159
+ optionsNode: TSESTree.ObjectExpression
160
+ ): void => {
161
+ for (const prop of optionsNode.properties) {
162
+ if (prop.type !== 'Property' || prop.key.type !== 'Identifier') {
163
+ continue;
164
+ }
165
+
166
+ const keyName = prop.key.name;
167
+ if (
168
+ keyName !== 'algorithms' &&
169
+ keyName !== 'algorithm' &&
170
+ keyName !== 'alg'
171
+ ) {
172
+ continue;
173
+ }
174
+
175
+ // Single algorithm: { algorithm: 'none' }
176
+ if (
177
+ prop.value.type === 'Literal' &&
178
+ typeof prop.value.value === 'string'
179
+ ) {
180
+ if (isNoneAlgorithm(prop.value.value)) {
181
+ context.report({
182
+ node: prop.value,
183
+ messageId: 'algorithmNone',
184
+ });
185
+ }
186
+ }
187
+
188
+ // Array of algorithms: { algorithms: ['none'] } or { algorithms: [] }
189
+ if (prop.value.type === 'ArrayExpression') {
190
+ // Check for empty array
191
+ if (prop.value.elements.length === 0) {
192
+ context.report({
193
+ node: prop.value,
194
+ messageId: 'emptyAlgorithms',
195
+ });
196
+ continue;
197
+ }
198
+
199
+ // Check each element for 'none'
200
+ for (const elem of prop.value.elements) {
201
+ if (
202
+ elem &&
203
+ elem.type === 'Literal' &&
204
+ typeof elem.value === 'string' &&
205
+ isNoneAlgorithm(elem.value)
206
+ ) {
207
+ context.report({
208
+ node: elem,
209
+ messageId: 'algorithmNoneInArray',
210
+ });
211
+ }
212
+ }
213
+ }
214
+ }
215
+ };
216
+
217
+ /**
218
+ * Detect `jwt.decode()` calls — closes the audit FN where
219
+ * `jwt.decode(token)` followed by trusting the decoded payload was
220
+ * silent, despite being functionally equivalent to algorithm:"none".
221
+ * See benchmarks/AUDIT_PATTERNS.md §3.8 ("verify-then-trust mismatch").
222
+ *
223
+ * The check is conservative: we flag every `jwt.decode()` call. In
224
+ * the rare case where `decode` is used legitimately (e.g. logging
225
+ * the issuer of an inbound token without authentication semantics),
226
+ * the user can disable the rule on that line. The default-on
227
+ * behaviour is correct — most production uses of `jwt.decode` are
228
+ * authentication mistakes.
229
+ */
230
+ // oxlint-disable-next-line consistent-function-scoping
231
+ function isDecodeCall(node: TSESTree.CallExpression): boolean {
232
+ const callee = node.callee;
233
+ if (callee.type !== 'MemberExpression') return false;
234
+ if (callee.property.type !== 'Identifier') return false;
235
+ if (callee.property.name !== 'decode') return false;
236
+ // Object should be `jwt` (or imported as such). Check both
237
+ // `jwt.decode` and `JWT.decode` patterns.
238
+ if (callee.object.type === 'Identifier') {
239
+ const name = callee.object.name.toLowerCase();
240
+ return name === 'jwt' || name === 'jsonwebtoken';
241
+ }
242
+ return false;
243
+ }
244
+
245
+ return {
246
+ CallExpression(node: TSESTree.CallExpression) {
247
+ // Detect `jwt.decode(...)` — equivalent to algorithm:"none" if
248
+ // its result is trusted. We always flag (with critical severity)
249
+ // because the FN risk far outweighs the FP risk on real code.
250
+ if (isDecodeCall(node)) {
251
+ context.report({ node, messageId: 'decodeWithoutVerify' });
252
+ return;
253
+ }
254
+
255
+ // Check both verify and sign operations
256
+ if (!isVerifyOperation(node) && !isSignOperation(node)) {
257
+ return;
258
+ }
259
+
260
+ // Get options argument (usually 3rd argument)
261
+ const optionsArg = getOptionsArgument(node, 2);
262
+ if (optionsArg) {
263
+ checkOptionsForNone(optionsArg);
264
+ }
265
+ },
266
+ };
267
+ },
268
+ });
269
+
270
+ export default noAlgorithmNone;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Tests for no-algorithm-none rule
3
+ * Security: CWE-347 (CVE-2022-23540)
4
+ */
5
+ import { RuleTester } from '@typescript-eslint/rule-tester';
6
+ import { describe, it, afterAll } from 'vitest';
7
+ import parser from '@typescript-eslint/parser';
8
+ import { noAlgorithmNone } from './index';
9
+
10
+ // Configure RuleTester for Vitest
11
+ RuleTester.afterAll = afterAll;
12
+ RuleTester.it = it;
13
+ RuleTester.itOnly = it.only;
14
+ RuleTester.describe = describe;
15
+
16
+ const ruleTester = new RuleTester({
17
+ languageOptions: {
18
+ parser,
19
+ ecmaVersion: 2022,
20
+ sourceType: 'module',
21
+ },
22
+ });
23
+
24
+ describe('no-algorithm-none', () => {
25
+ describe('Valid Code - Secure Algorithms', () => {
26
+ ruleTester.run('valid - secure algorithms', noAlgorithmNone, {
27
+ valid: [
28
+ // RS256 - RSA with SHA-256
29
+ { code: `jwt.verify(token, publicKey, { algorithms: ['RS256'] });` },
30
+ // ES256 - ECDSA with SHA-256
31
+ { code: `jwt.verify(token, publicKey, { algorithms: ['ES256'] });` },
32
+ // Multiple secure algorithms
33
+ { code: `jwt.verify(token, key, { algorithms: ['RS256', 'RS384', 'RS512'] });` },
34
+ // Single algorithm option
35
+ { code: `jwt.verify(token, secret, { algorithm: 'HS256' });` },
36
+ // jose library
37
+ { code: `jwtVerify(token, key, { algorithms: ['RS256'] });` },
38
+ // No options (doesn't trigger this rule)
39
+ { code: `jwt.verify(token, secret);` },
40
+ // Sign operation with secure algorithm
41
+ { code: `jwt.sign(payload, secret, { algorithm: 'RS256' });` },
42
+ // Spread operator in options (edge case - line 143 coverage)
43
+ { code: `jwt.verify(token, key, { ...opts, algorithms: ['RS256'] });` },
44
+ // Non-algorithm properties (line 152 coverage)
45
+ { code: `jwt.verify(token, key, { issuer: 'auth', complete: true });` },
46
+ // Options without algorithms (different property names)
47
+ { code: `jwt.sign(payload, secret, { expiresIn: '1h', subject: 'user' });` },
48
+ // Variable algorithms (not literal)
49
+ { code: `jwt.verify(token, key, { algorithms: allowedAlgorithms });` },
50
+ // Array with non-literal elements
51
+ { code: `jwt.verify(token, key, { algorithms: [defaultAlg] });` },
52
+ // Non-string literal in array
53
+ { code: `jwt.verify(token, key, { algorithms: [123] });` },
54
+ // Non-JWT function call - edge case (line 199 coverage)
55
+ { code: `console.log('hello');` },
56
+ { code: `someOtherFunction(token, key, { algorithm: 'none' });` },
57
+ ],
58
+ invalid: [],
59
+ });
60
+ });
61
+
62
+ describe('Invalid Code - Algorithm None', () => {
63
+ ruleTester.run('invalid - algorithm none', noAlgorithmNone, {
64
+ valid: [],
65
+ invalid: [
66
+ // Direct 'none' algorithm
67
+ {
68
+ code: `jwt.verify(token, secret, { algorithm: 'none' });`,
69
+ errors: [{ messageId: 'algorithmNone' }],
70
+ },
71
+ // 'none' in algorithms array (single element - gets replaced)
72
+ {
73
+ code: `jwt.verify(token, secret, { algorithms: ['none'] });`,
74
+ errors: [{ messageId: 'algorithmNoneInArray' }],
75
+ },
76
+ // 'none' mixed with other algorithms (no auto-fix for removal)
77
+ {
78
+ code: `jwt.verify(token, secret, { algorithms: ['RS256', 'none'] });`,
79
+ errors: [{ messageId: 'algorithmNoneInArray' }],
80
+ },
81
+ // Case insensitive - 'None'
82
+ {
83
+ code: `jwt.verify(token, secret, { algorithm: 'None' });`,
84
+ errors: [{ messageId: 'algorithmNone' }],
85
+ },
86
+ // Case insensitive - 'NONE'
87
+ {
88
+ code: `jwt.verify(token, secret, { algorithms: ['NONE'] });`,
89
+ errors: [{ messageId: 'algorithmNoneInArray' }],
90
+ },
91
+ // alg shorthand
92
+ {
93
+ code: `jwt.verify(token, secret, { alg: 'none' });`,
94
+ errors: [{ messageId: 'algorithmNone' }],
95
+ },
96
+ // Empty algorithms array
97
+ {
98
+ code: `jwt.verify(token, secret, { algorithms: [] });`,
99
+ errors: [{ messageId: 'emptyAlgorithms' }],
100
+ },
101
+ // Sign operation with none
102
+ {
103
+ code: `jwt.sign(payload, secret, { algorithm: 'none' });`,
104
+ errors: [{ messageId: 'algorithmNone' }],
105
+ },
106
+ // jose library
107
+ {
108
+ code: `jwtVerify(token, key, { algorithms: ['none'] });`,
109
+ errors: [{ messageId: 'algorithmNoneInArray' }],
110
+ },
111
+ // signJWT with none
112
+ {
113
+ code: `signJWT(payload, key, { algorithm: 'none' });`,
114
+ errors: [{ messageId: 'algorithmNone' }],
115
+ },
116
+ ],
117
+ });
118
+ });
119
+
120
+ describe('Configuration Options', () => {
121
+ ruleTester.run('config - allowInTests', noAlgorithmNone, {
122
+ valid: [
123
+ // Test file with allowInTests enabled
124
+ {
125
+ code: `jwt.verify(token, secret, { algorithm: 'none' });`,
126
+ options: [{ allowInTests: true }],
127
+ filename: 'auth.test.ts',
128
+ },
129
+ {
130
+ code: `jwt.verify(token, secret, { algorithm: 'none' });`,
131
+ options: [{ allowInTests: true }],
132
+ filename: '__tests__/jwt.ts',
133
+ },
134
+ ],
135
+ invalid: [
136
+ // Non-test file with allowInTests
137
+ {
138
+ code: `jwt.verify(token, secret, { algorithm: 'none' });`,
139
+ options: [{ allowInTests: true }],
140
+ filename: 'auth.ts',
141
+ errors: [{ messageId: 'algorithmNone' }],
142
+ },
143
+ ],
144
+ });
145
+ });
146
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Copyright (c) 2025 Ofri Peretz
3
+ * Licensed under the MIT License. Use of this source code is governed by the
4
+ * MIT license that can be found in the LICENSE file.
5
+ */
6
+
7
+ /**
8
+ * ESLint Rule: no-decode-without-verify
9
+ *
10
+ * Detects usage of jwt.decode() or jwt-decode library without corresponding
11
+ * verification. Decoded JWTs can be tampered with by attackers.
12
+ *
13
+ * CWE-345: Insufficient Verification of Data Authenticity
14
+ *
15
+ * @see https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JSON_Web_Tokens
16
+ */
17
+ import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
18
+ import { createRule, formatLLMMessage, MessageIcons, hasSafeAnnotation } from '@interlace/eslint-devkit';
19
+ import { isDecodeOperation } from '../../utils';
20
+ import type { NoDecodeWithoutVerifyOptions } from '../../types';
21
+
22
+ type MessageIds =
23
+ | 'decodeWithoutVerify'
24
+ | 'jwtDecodeLibrary'
25
+ | 'useVerifyInstead';
26
+
27
+ type RuleOptions = [NoDecodeWithoutVerifyOptions?];
28
+
29
+ export const noDecodeWithoutVerify = createRule<RuleOptions, MessageIds>({
30
+ name: 'no-decode-without-verify',
31
+ meta: {
32
+ type: 'problem',
33
+ docs: {
34
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-decode-without-verify.md',
35
+ description:
36
+ 'Disallow trusting decoded JWT payload without signature verification',
37
+ cwe: 'CWE-345',
38
+ cvss: 7.5,
39
+ },
40
+ fixable: undefined,
41
+ hasSuggestions: false,
42
+ messages: {
43
+ decodeWithoutVerify: formatLLMMessage({
44
+ icon: MessageIcons.SECURITY,
45
+ issueName: 'Decoded JWT Without Verification',
46
+ cwe: 'CWE-345',
47
+ description:
48
+ 'jwt.decode() returns payload without verifying signature - data can be forged',
49
+ severity: 'HIGH',
50
+ fix: 'Use jwt.verify(token, secret) instead of jwt.decode(token)',
51
+ documentationLink:
52
+ 'https://owasp.org/API-Security/0xa7-security-misconfiguration/',
53
+ }),
54
+ jwtDecodeLibrary: formatLLMMessage({
55
+ icon: MessageIcons.SECURITY,
56
+ issueName: 'jwt-decode Library Usage',
57
+ cwe: 'CWE-345',
58
+ description:
59
+ 'jwt-decode library only decodes tokens, never verifies signatures',
60
+ severity: 'HIGH',
61
+ fix: 'Use jsonwebtoken.verify() or jose.jwtVerify() for verification',
62
+ documentationLink: 'https://www.npmjs.com/package/jwt-decode',
63
+ }),
64
+ useVerifyInstead: formatLLMMessage({
65
+ icon: MessageIcons.INFO,
66
+ issueName: 'Use Verify Instead',
67
+ description: 'Replace decode with verify to ensure authenticity',
68
+ severity: 'LOW',
69
+ fix: 'jwt.verify(token, secret, { algorithms: ["RS256"] })',
70
+ documentationLink: 'https://www.npmjs.com/package/jsonwebtoken',
71
+ }),
72
+ },
73
+ schema: [
74
+ {
75
+ type: 'object',
76
+ properties: {
77
+ allowHeaderInspection: {
78
+ type: 'boolean',
79
+ default: false,
80
+ description:
81
+ 'Allow decode() for reading header before verification',
82
+ },
83
+ trustedSanitizers: {
84
+ type: 'array',
85
+ items: { type: 'string' },
86
+ default: [],
87
+ },
88
+ trustedAnnotations: {
89
+ type: 'array',
90
+ items: { type: 'string' },
91
+ default: ['@decoded-header-only', '@verified-separately'],
92
+ },
93
+ strictMode: {
94
+ type: 'boolean',
95
+ default: false,
96
+ },
97
+ },
98
+ additionalProperties: false,
99
+ },
100
+ ],
101
+ },
102
+ defaultOptions: [
103
+ {
104
+ allowHeaderInspection: false,
105
+ trustedSanitizers: [],
106
+ trustedAnnotations: ['@decoded-header-only', '@verified-separately'],
107
+ strictMode: false,
108
+ },
109
+ ],
110
+ create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
111
+ const options = context.options[0] ?? {};
112
+ const { trustedAnnotations = [] } = options;
113
+
114
+ /**
115
+ * Check if this is a jwt-decode import usage
116
+ */
117
+ // oxlint-disable-next-line consistent-function-scoping
118
+ const isJwtDecodeLibrary = (node: TSESTree.CallExpression): boolean => {
119
+ // Direct call: jwtDecode(token)
120
+ if (
121
+ node.callee.type === 'Identifier' &&
122
+ (node.callee.name === 'jwtDecode' || node.callee.name === 'jwt_decode')
123
+ ) {
124
+ return true;
125
+ }
126
+ return false;
127
+ };
128
+
129
+ return {
130
+ CallExpression(node: TSESTree.CallExpression) {
131
+ // Check for jwt.decode() pattern
132
+ if (isDecodeOperation(node)) {
133
+ // Check for safe annotations
134
+ if (hasSafeAnnotation(node, context, trustedAnnotations)) {
135
+ return;
136
+ }
137
+
138
+ context.report({
139
+ node,
140
+ messageId: 'decodeWithoutVerify',
141
+ });
142
+ return;
143
+ }
144
+
145
+ // Check for jwt-decode library usage
146
+ if (isJwtDecodeLibrary(node)) {
147
+ // Check for safe annotations
148
+ if (hasSafeAnnotation(node, context, trustedAnnotations)) {
149
+ return;
150
+ }
151
+
152
+ context.report({
153
+ node,
154
+ messageId: 'jwtDecodeLibrary',
155
+ });
156
+ }
157
+ },
158
+ };
159
+ },
160
+ });
161
+
162
+ export default noDecodeWithoutVerify;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Tests for no-decode-without-verify rule
3
+ * Security: CWE-345 (Data Authenticity)
4
+ */
5
+ import { RuleTester } from '@typescript-eslint/rule-tester';
6
+ import { describe, it, afterAll } from 'vitest';
7
+ import parser from '@typescript-eslint/parser';
8
+ import { noDecodeWithoutVerify } from './index';
9
+
10
+ RuleTester.afterAll = afterAll;
11
+ RuleTester.it = it;
12
+ RuleTester.itOnly = it.only;
13
+ RuleTester.describe = describe;
14
+
15
+ const ruleTester = new RuleTester({
16
+ languageOptions: {
17
+ parser,
18
+ ecmaVersion: 2022,
19
+ sourceType: 'module',
20
+ },
21
+ });
22
+
23
+ describe('no-decode-without-verify', () => {
24
+ describe('Valid Code - Verify Operations', () => {
25
+ ruleTester.run('valid - verify operations', noDecodeWithoutVerify, {
26
+ valid: [
27
+ // jwt.verify is safe
28
+ { code: `const payload = jwt.verify(token, secret);` },
29
+ { code: `jwt.verify(token, publicKey, { algorithms: ['RS256'] });` },
30
+ // jose jwtVerify is safe
31
+ { code: `const { payload } = await jwtVerify(token, key);` },
32
+ // sign is not flagged
33
+ { code: `jwt.sign(payload, secret);` },
34
+ ],
35
+ invalid: [],
36
+ });
37
+ });
38
+
39
+ describe('Invalid Code - Decode Operations', () => {
40
+ ruleTester.run('invalid - decode without verify', noDecodeWithoutVerify, {
41
+ valid: [],
42
+ invalid: [
43
+ // Basic jwt.decode()
44
+ {
45
+ code: `const payload = jwt.decode(token);`,
46
+ errors: [{ messageId: 'decodeWithoutVerify' }],
47
+ },
48
+ // decode with options
49
+ {
50
+ code: `const decoded = jwt.decode(token, { complete: true });`,
51
+ errors: [{ messageId: 'decodeWithoutVerify' }],
52
+ },
53
+ // Using payload directly
54
+ {
55
+ code: `const userId = jwt.decode(token).sub;`,
56
+ errors: [{ messageId: 'decodeWithoutVerify' }],
57
+ },
58
+ // jwt-decode library - jwtDecode pattern
59
+ {
60
+ code: `const payload = jwtDecode(accessToken);`,
61
+ errors: [{ messageId: 'decodeWithoutVerify' }],
62
+ },
63
+ // jwt-decode library - jwt_decode pattern (snake_case import)
64
+ {
65
+ code: `const payload = jwt_decode(accessToken);`,
66
+ errors: [{ messageId: 'jwtDecodeLibrary' }],
67
+ },
68
+ // jose decodeJwt (decode only)
69
+ {
70
+ code: `const payload = decodeJWT(token);`,
71
+ errors: [{ messageId: 'decodeWithoutVerify' }],
72
+ },
73
+ ],
74
+ });
75
+ });
76
+ });