eslint-plugin-jwt-security 2.3.0 → 2.3.2

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 (97) hide show
  1. package/README.md +5 -5
  2. package/package.json +16 -24
  3. package/src/index.d.ts +17 -0
  4. package/src/index.js +1 -0
  5. package/src/oxlint.d.ts +17 -0
  6. package/src/oxlint.js +1 -0
  7. package/src/rules/no-algorithm-confusion/index.js +1 -0
  8. package/src/rules/no-algorithm-none/index.js +1 -0
  9. package/src/rules/no-decode-without-verify/index.js +1 -0
  10. package/src/rules/no-hardcoded-secret/index.js +1 -0
  11. package/src/rules/no-sensitive-payload/index.js +1 -0
  12. package/src/rules/no-timestamp-manipulation/index.js +1 -0
  13. package/src/rules/no-weak-secret/index.js +1 -0
  14. package/src/rules/require-algorithm-whitelist/index.js +1 -0
  15. package/src/rules/require-audience-validation/index.js +1 -0
  16. package/src/rules/require-expiration/index.js +1 -0
  17. package/src/rules/require-issued-at/index.js +1 -0
  18. package/src/rules/require-issuer-validation/index.js +1 -0
  19. package/src/rules/require-max-age/index.js +1 -0
  20. package/src/types/index.js +1 -0
  21. package/src/utils/index.js +1 -0
  22. package/AGENTS.md +0 -102
  23. package/CHANGELOG.md +0 -161
  24. package/dist/AGENTS.md +0 -102
  25. package/dist/CHANGELOG.md +0 -161
  26. package/dist/LICENSE +0 -23
  27. package/dist/README.md +0 -158
  28. package/dist/package.json +0 -125
  29. package/dist/src/index.d.ts +0 -101
  30. package/dist/src/index.js +0 -171
  31. package/dist/src/oxlint.d.ts +0 -20
  32. package/dist/src/oxlint.js +0 -21
  33. package/dist/src/rules/no-algorithm-confusion/index.d.ts +0 -24
  34. package/dist/src/rules/no-algorithm-confusion/index.js +0 -165
  35. package/dist/src/rules/no-algorithm-none/index.d.ts +0 -24
  36. package/dist/src/rules/no-algorithm-none/index.js +0 -222
  37. package/dist/src/rules/no-decode-without-verify/index.d.ts +0 -23
  38. package/dist/src/rules/no-decode-without-verify/index.js +0 -131
  39. package/dist/src/rules/no-hardcoded-secret/index.d.ts +0 -23
  40. package/dist/src/rules/no-hardcoded-secret/index.js +0 -189
  41. package/dist/src/rules/no-sensitive-payload/index.d.ts +0 -20
  42. package/dist/src/rules/no-sensitive-payload/index.js +0 -103
  43. package/dist/src/rules/no-timestamp-manipulation/index.d.ts +0 -23
  44. package/dist/src/rules/no-timestamp-manipulation/index.js +0 -97
  45. package/dist/src/rules/no-weak-secret/index.d.ts +0 -23
  46. package/dist/src/rules/no-weak-secret/index.js +0 -165
  47. package/dist/src/rules/require-algorithm-whitelist/index.d.ts +0 -24
  48. package/dist/src/rules/require-algorithm-whitelist/index.js +0 -113
  49. package/dist/src/rules/require-audience-validation/index.d.ts +0 -20
  50. package/dist/src/rules/require-audience-validation/index.js +0 -100
  51. package/dist/src/rules/require-expiration/index.d.ts +0 -23
  52. package/dist/src/rules/require-expiration/index.js +0 -136
  53. package/dist/src/rules/require-issued-at/index.d.ts +0 -23
  54. package/dist/src/rules/require-issued-at/index.js +0 -113
  55. package/dist/src/rules/require-issuer-validation/index.d.ts +0 -20
  56. package/dist/src/rules/require-issuer-validation/index.js +0 -100
  57. package/dist/src/rules/require-max-age/index.d.ts +0 -24
  58. package/dist/src/rules/require-max-age/index.js +0 -93
  59. package/dist/src/types/index.js +0 -7
  60. package/dist/src/utils/index.d.ts +0 -98
  61. package/dist/src/utils/index.js +0 -238
  62. package/src/__compatibility__/jwt-interface.spec.ts +0 -192
  63. package/src/index.test.ts +0 -47
  64. package/src/index.ts +0 -209
  65. package/src/oxlint.test.ts +0 -61
  66. package/src/oxlint.ts +0 -23
  67. package/src/rules/no-algorithm-confusion/index.ts +0 -208
  68. package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +0 -106
  69. package/src/rules/no-algorithm-none/index.ts +0 -270
  70. package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +0 -146
  71. package/src/rules/no-decode-without-verify/index.ts +0 -162
  72. package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +0 -76
  73. package/src/rules/no-hardcoded-secret/index.ts +0 -230
  74. package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +0 -97
  75. package/src/rules/no-sensitive-payload/index.ts +0 -123
  76. package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +0 -157
  77. package/src/rules/no-timestamp-manipulation/index.ts +0 -118
  78. package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +0 -54
  79. package/src/rules/no-weak-secret/index.ts +0 -204
  80. package/src/rules/no-weak-secret/no-weak-secret.test.ts +0 -114
  81. package/src/rules/require-algorithm-whitelist/index.ts +0 -145
  82. package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +0 -72
  83. package/src/rules/require-audience-validation/index.ts +0 -120
  84. package/src/rules/require-audience-validation/require-audience-validation.test.ts +0 -68
  85. package/src/rules/require-expiration/index.ts +0 -170
  86. package/src/rules/require-expiration/require-expiration.test.ts +0 -135
  87. package/src/rules/require-issued-at/index.ts +0 -143
  88. package/src/rules/require-issued-at/require-issued-at.test.ts +0 -64
  89. package/src/rules/require-issuer-validation/index.ts +0 -120
  90. package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +0 -68
  91. package/src/rules/require-max-age/index.ts +0 -117
  92. package/src/rules/require-max-age/require-max-age.test.ts +0 -69
  93. package/src/tests/coverage-dual-layer.test.ts +0 -323
  94. package/src/types/index.ts +0 -107
  95. package/src/utils/index.ts +0 -285
  96. package/src/utils/utils.test.ts +0 -324
  97. /package/{dist/src → src}/types/index.d.ts +0 -0
@@ -1,68 +0,0 @@
1
- /**
2
- * Tests for require-issuer-validation rule
3
- */
4
- import { RuleTester } from '@typescript-eslint/rule-tester';
5
- import { describe, it, afterAll } from 'vitest';
6
- import parser from '@typescript-eslint/parser';
7
- import { requireIssuerValidation } from './index';
8
-
9
- RuleTester.afterAll = afterAll;
10
- RuleTester.it = it;
11
- RuleTester.itOnly = it.only;
12
- RuleTester.describe = describe;
13
-
14
- const ruleTester = new RuleTester({
15
- languageOptions: {
16
- parser,
17
- ecmaVersion: 2022,
18
- sourceType: 'module',
19
- },
20
- });
21
-
22
- describe('require-issuer-validation', () => {
23
- describe('Valid Code', () => {
24
- ruleTester.run('valid - with issuer', requireIssuerValidation, {
25
- valid: [
26
- { code: `jwt.verify(token, secret, { issuer: 'https://auth.example.com' });` },
27
- { code: `jwt.verify(token, secret, { iss: 'auth.example.com' });` },
28
- { code: `jwt.verify(token, secret, { issuer: 'auth', algorithms: ['RS256'] });` },
29
- { code: `jwt.sign(payload, secret);` }, // sign not checked
30
- // Only one argument - edge case
31
- { code: `jwt.verify(token);` },
32
- // jwtVerify with issuer
33
- { code: `jwtVerify(token, key, { issuer: 'https://auth.example.com' });` },
34
- ],
35
- invalid: [],
36
- });
37
- });
38
-
39
- describe('Invalid Code', () => {
40
- ruleTester.run('invalid - missing issuer', requireIssuerValidation, {
41
- valid: [],
42
- invalid: [
43
- {
44
- code: `jwt.verify(token, secret);`,
45
- errors: [{ messageId: 'missingIssuerValidation' }],
46
- },
47
- {
48
- code: `jwt.verify(token, secret, {});`,
49
- errors: [{ messageId: 'missingIssuerValidation' }],
50
- },
51
- {
52
- code: `jwt.verify(token, secret, { algorithms: ['RS256'] });`,
53
- errors: [{ messageId: 'missingIssuerValidation' }],
54
- },
55
- // jwtVerify without issuer
56
- {
57
- code: `jwtVerify(token, key);`,
58
- errors: [{ messageId: 'missingIssuerValidation' }],
59
- },
60
- // jwtVerify with empty options
61
- {
62
- code: `jwtVerify(token, key, {});`,
63
- errors: [{ messageId: 'missingIssuerValidation' }],
64
- },
65
- ],
66
- });
67
- });
68
- });
@@ -1,117 +0,0 @@
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: require-max-age
9
- *
10
- * Mandates maxAge option in JWT verify operations to enforce token freshness.
11
- * This prevents the "Back to the Future" replay attack where tokens are
12
- * captured and replayed months or years later.
13
- *
14
- * CWE-294: Authentication Bypass by Capture-replay
15
- *
16
- * @see https://securitypattern.com/post/jwt-back-to-the-future
17
- * @see LightSEC 2025 - "JWT Back to the future: On the (ab)use of JWTs"
18
- */
19
- import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
20
- import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
21
- import { isVerifyOperation, getOptionsArgument, hasOption } from '../../utils';
22
- import type { JwtRuleOptions } from '../../types';
23
-
24
- type MessageIds = 'missingMaxAge';
25
-
26
- type RuleOptions = [JwtRuleOptions?];
27
-
28
- export const requireMaxAge = createRule<RuleOptions, MessageIds>({
29
- name: 'require-max-age',
30
- meta: {
31
- type: 'problem',
32
- docs: {
33
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-max-age.md',
34
- description:
35
- 'Require maxAge option in verify operations to enforce token freshness',
36
- cwe: 'CWE-294',
37
- cvss: 5,
38
- },
39
- fixable: undefined,
40
- hasSuggestions: false,
41
- messages: {
42
- missingMaxAge: formatLLMMessage({
43
- icon: MessageIcons.SECURITY,
44
- issueName: 'Missing Token Age Validation',
45
- cwe: 'CWE-294',
46
- description:
47
- 'Without maxAge, tokens can be replayed long after issuance (Back to the Future attack)',
48
- severity: 'MEDIUM',
49
- fix: 'Add maxAge option: { maxAge: "1h" } to limit token lifetime',
50
- documentationLink: 'https://securitypattern.com/post/jwt-back-to-the-future',
51
- }),
52
- },
53
- schema: [
54
- {
55
- type: 'object',
56
- properties: {
57
- trustedSanitizers: {
58
- type: 'array',
59
- items: { type: 'string' },
60
- default: [],
61
- },
62
- trustedAnnotations: {
63
- type: 'array',
64
- items: { type: 'string' },
65
- default: [],
66
- },
67
- strictMode: {
68
- type: 'boolean',
69
- default: false,
70
- },
71
- },
72
- additionalProperties: false,
73
- },
74
- ],
75
- },
76
- defaultOptions: [
77
- {
78
- trustedSanitizers: [],
79
- trustedAnnotations: [],
80
- strictMode: false,
81
- },
82
- ],
83
- create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
84
- return {
85
- CallExpression(node: TSESTree.CallExpression) {
86
- if (!isVerifyOperation(node)) {
87
- return;
88
- }
89
-
90
- if (node.arguments.length < 2) {
91
- return;
92
- }
93
-
94
- const optionsArg = getOptionsArgument(node, 2);
95
-
96
- // No options at all
97
- if (!optionsArg) {
98
- context.report({
99
- node,
100
- messageId: 'missingMaxAge',
101
- });
102
- return;
103
- }
104
-
105
- // Check for maxAge option
106
- if (!hasOption(optionsArg, 'maxAge') && !hasOption(optionsArg, 'clockTolerance')) {
107
- context.report({
108
- node: optionsArg,
109
- messageId: 'missingMaxAge',
110
- });
111
- }
112
- },
113
- };
114
- },
115
- });
116
-
117
- export default requireMaxAge;
@@ -1,69 +0,0 @@
1
- /**
2
- * Tests for require-max-age rule
3
- * Security: LightSEC 2025 - Replay Attack Prevention
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 { requireMaxAge } 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('require-max-age', () => {
24
- describe('Valid Code', () => {
25
- ruleTester.run('valid - with maxAge', requireMaxAge, {
26
- valid: [
27
- { code: `jwt.verify(token, secret, { maxAge: '1h' });` },
28
- { code: `jwt.verify(token, secret, { maxAge: 3600 });` },
29
- { code: `jwt.verify(token, secret, { clockTolerance: 30 });` },
30
- { code: `jwt.sign(payload, secret);` }, // sign not checked
31
- // Only one argument - edge case
32
- { code: `jwt.verify(token);` },
33
- // jwtVerify with maxAge
34
- { code: `jwtVerify(token, key, { maxAge: '24h' });` },
35
- ],
36
- invalid: [],
37
- });
38
- });
39
-
40
- describe('Invalid Code', () => {
41
- ruleTester.run('invalid - missing maxAge', requireMaxAge, {
42
- valid: [],
43
- invalid: [
44
- {
45
- code: `jwt.verify(token, secret);`,
46
- errors: [{ messageId: 'missingMaxAge' }],
47
- },
48
- {
49
- code: `jwt.verify(token, secret, {});`,
50
- errors: [{ messageId: 'missingMaxAge' }],
51
- },
52
- {
53
- code: `jwt.verify(token, secret, { algorithms: ['RS256'] });`,
54
- errors: [{ messageId: 'missingMaxAge' }],
55
- },
56
- // jwtVerify without maxAge
57
- {
58
- code: `jwtVerify(token, key);`,
59
- errors: [{ messageId: 'missingMaxAge' }],
60
- },
61
- // verifyJWT without maxAge
62
- {
63
- code: `verifyJWT(token, key, { algorithms: ['RS256'] });`,
64
- errors: [{ messageId: 'missingMaxAge' }],
65
- },
66
- ],
67
- });
68
- });
69
- });
@@ -1,323 +0,0 @@
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
- * Dual-layer coverage tests — closes the remaining uncovered branches.
9
- *
10
- * Layer 1: RuleTester fixtures through the real parser.
11
- * Layer 2: raw unit tests — helpers called as plain functions, plus
12
- * createWithMockContext (from @interlace/eslint-devkit) invoking
13
- * rule listeners directly with synthetic AST nodes for
14
- * parser-unreachable branches.
15
- */
16
- import { RuleTester } from '@typescript-eslint/rule-tester';
17
- import { describe, it, expect, afterAll } from 'vitest';
18
- import parser from '@typescript-eslint/parser';
19
- import type { TSESTree } from '@interlace/eslint-devkit';
20
- import { createWithMockContext } from '@interlace/eslint-devkit';
21
- import { noAlgorithmConfusion } from '../rules/no-algorithm-confusion';
22
- import { noAlgorithmNone } from '../rules/no-algorithm-none';
23
- import { noDecodeWithoutVerify } from '../rules/no-decode-without-verify';
24
- import { noHardcodedSecret } from '../rules/no-hardcoded-secret';
25
- import { noSensitivePayload } from '../rules/no-sensitive-payload';
26
- import { noWeakSecret } from '../rules/no-weak-secret';
27
- import { extractAlgorithms } from '../utils';
28
-
29
- RuleTester.afterAll = afterAll;
30
- RuleTester.it = it;
31
- RuleTester.itOnly = it.only;
32
- RuleTester.describe = describe;
33
-
34
- const ruleTester = new RuleTester({
35
- languageOptions: {
36
- parser,
37
- ecmaVersion: 2022,
38
- sourceType: 'module',
39
- },
40
- });
41
-
42
- // ---------------------------------------------------------------------------
43
- // Layer 1 — no-algorithm-confusion: property-loop guards (spread + computed
44
- // key before the `algorithms` entry) still land the report on the right node.
45
- // ---------------------------------------------------------------------------
46
- describe('coverage: no-algorithm-confusion property-loop guards', () => {
47
- ruleTester.run('spread and computed props before algorithms', noAlgorithmConfusion, {
48
- valid: [],
49
- invalid: [
50
- {
51
- // SpreadElement (prop.type !== 'Property') and computed key
52
- // (prop.key.type !== 'Identifier') are skipped; the report fires on
53
- // the `algorithms` property value.
54
- code: `jwt.verify(token, publicKey, { ...baseOpts, ['audit']: true, algorithms: ['HS256'] });`,
55
- errors: [{ messageId: 'algorithmConfusion' }],
56
- },
57
- ],
58
- });
59
- });
60
-
61
- // ---------------------------------------------------------------------------
62
- // Layer 1 — no-algorithm-none: the isDecodeCall() detector.
63
- // ---------------------------------------------------------------------------
64
- describe('coverage: no-algorithm-none decode detection', () => {
65
- ruleTester.run('isDecodeCall branches', noAlgorithmNone, {
66
- valid: [
67
- // object name is neither `jwt` nor `jsonwebtoken` -> not a decode call
68
- { code: `myParser.decode(data);` },
69
- // callee object is itself a MemberExpression -> returns false
70
- { code: `api.jwt.decode(token);` },
71
- // computed property (Literal, not Identifier) -> returns false
72
- { code: `jwt["decode"](token);` },
73
- ],
74
- invalid: [
75
- // jwt.decode() is flagged as equivalent to algorithm:"none"
76
- {
77
- code: `jwt.decode(token);`,
78
- errors: [{ messageId: 'decodeWithoutVerify' }],
79
- },
80
- // jsonwebtoken.decode() — second operand of the name check
81
- {
82
- code: `jsonwebtoken.decode(token);`,
83
- errors: [{ messageId: 'decodeWithoutVerify' }],
84
- },
85
- ],
86
- });
87
- });
88
-
89
- // ---------------------------------------------------------------------------
90
- // Layer 1 — no-decode-without-verify: safe-annotation suppression (previously
91
- // hidden behind c8-ignore; a plain leading comment through the real parser
92
- // exercises hasSafeAnnotation for both the jwt.decode and jwt_decode paths).
93
- // ---------------------------------------------------------------------------
94
- describe('coverage: no-decode-without-verify safe annotations', () => {
95
- ruleTester.run('trusted annotations suppress the report', noDecodeWithoutVerify, {
96
- valid: [
97
- {
98
- // custom trustedAnnotations option suppresses the jwt.decode report
99
- code: `// @decoded-header-only
100
- const header = jwt.decode(token);`,
101
- options: [{ trustedAnnotations: ['@decoded-header-only'] }],
102
- },
103
- {
104
- // built-in devkit SAFE_ANNOTATIONS ('@verified') suppresses jwt_decode
105
- code: `/* @verified-separately */
106
- const payload = jwt_decode(token);`,
107
- },
108
- ],
109
- invalid: [
110
- // Unrelated leading comment does NOT suppress — annotation walk runs
111
- // and falls through to the report.
112
- {
113
- code: `// TODO: refactor later
114
- const payload = jwt.decode(token);`,
115
- errors: [{ messageId: 'decodeWithoutVerify' }],
116
- },
117
- {
118
- code: `// just a note
119
- const payload = jwt_decode(token);`,
120
- errors: [{ messageId: 'jwtDecodeLibrary' }],
121
- },
122
- ],
123
- });
124
- });
125
-
126
- // ---------------------------------------------------------------------------
127
- // Layer 1 — no-hardcoded-secret: resolveConstLiteralValue() through the real
128
- // scope manager.
129
- // ---------------------------------------------------------------------------
130
- describe('coverage: no-hardcoded-secret const-literal resolution', () => {
131
- ruleTester.run('single-frame const indirection', noHardcodedSecret, {
132
- valid: [
133
- // `let` binding is never resolved (kind !== 'const') -> treated safe
134
- { code: `let MUTABLE_SECRET = 'abc'; jwt.sign(payload, MUTABLE_SECRET);` },
135
- // function parameter: def.type is 'Parameter', not 'Variable' -> safe
136
- { code: `function issue(SECRET) { return jwt.sign(payload, SECRET); }` },
137
- // unresolved global identifier: no def at all -> safe
138
- { code: `jwt.sign(payload, GLOBAL_SECRET);` },
139
- // for-of const has no initializer: `def.node.init ?? null` -> null -> safe
140
- { code: `for (const SECRET of secretList) { jwt.sign(payload, SECRET); }` },
141
- // const resolved to a non-literal initializer (call) -> safe
142
- { code: `const KEY = loadKey(); jwt.sign(payload, KEY);` },
143
- // non-identifier, non-literal secret arg: resolveConstLiteralValue()
144
- // gets a non-Identifier node via isHardcodedStringOrResolvedConst -> null
145
- { code: `jwt.sign(payload, [42]);` },
146
- ],
147
- invalid: [
148
- // const-hidden string literal is resolved and flagged
149
- {
150
- code: `const SECRET = 'my-hardcoded-secret'; jwt.sign(payload, SECRET);`,
151
- errors: [{ messageId: 'hardcodedSecret' }],
152
- },
153
- // const-hidden expressionless template literal is resolved and flagged
154
- {
155
- code: 'const SECRET = `template-secret`; jwt.sign(payload, SECRET);',
156
- errors: [{ messageId: 'hardcodedSecret' }],
157
- },
158
- ],
159
- });
160
- });
161
-
162
- // ---------------------------------------------------------------------------
163
- // Layer 1 — no-sensitive-payload: property-loop guards.
164
- // ---------------------------------------------------------------------------
165
- describe('coverage: no-sensitive-payload property-loop guards', () => {
166
- ruleTester.run('spread and non-identifier keys are skipped', noSensitivePayload, {
167
- valid: [
168
- // SpreadElement (prop.type !== 'Property') and string-literal key
169
- // (prop.key.type !== 'Identifier') are both skipped without reporting.
170
- {
171
- code: `jwt.sign({ ...claims, 'display-name': name, role: 'admin' }, process.env.JWT_SECRET);`,
172
- },
173
- ],
174
- invalid: [],
175
- });
176
- });
177
-
178
- // ---------------------------------------------------------------------------
179
- // Layer 1 — no-weak-secret: template-literal arms + missing-secret guard.
180
- // ---------------------------------------------------------------------------
181
- describe('coverage: no-weak-secret template literals and arg guard', () => {
182
- ruleTester.run('template secret arms', noWeakSecret, {
183
- valid: [
184
- // sign() with a single argument: secret check is skipped entirely
185
- { code: `jwt.sign(payload);` },
186
- // strong 34-char template literal: neither weak pattern nor short
187
- { code: 'jwt.sign(payload, `ThisIsAVeryStrongSecretThatIs32Ch+`);' },
188
- ],
189
- invalid: [
190
- // known weak pattern in a template literal (left arm true)
191
- {
192
- code: 'jwt.sign(payload, `secret`);',
193
- errors: [{ messageId: 'weakSecret' }],
194
- },
195
- // short but not a known pattern (left arm false, right arm true)
196
- {
197
- code: 'jwt.sign(payload, `xy9z`);',
198
- errors: [{ messageId: 'weakSecret' }],
199
- },
200
- ],
201
- });
202
- });
203
-
204
- // ---------------------------------------------------------------------------
205
- // Layer 2 — utils.extractAlgorithms: sparse-array holes and non-string
206
- // elements inside the algorithms array are skipped.
207
- // ---------------------------------------------------------------------------
208
- describe('coverage (unit): extractAlgorithms skips holes and non-strings', () => {
209
- it('ignores null elements and non-string literals', () => {
210
- const optionsNode = {
211
- type: 'ObjectExpression',
212
- properties: [
213
- {
214
- type: 'Property',
215
- key: { type: 'Identifier', name: 'algorithms' },
216
- value: {
217
- type: 'ArrayExpression',
218
- elements: [
219
- null, // sparse-array hole
220
- { type: 'Literal', value: 42 }, // non-string literal
221
- { type: 'Literal', value: 'RS256' },
222
- ],
223
- },
224
- },
225
- ],
226
- } as unknown as TSESTree.ObjectExpression;
227
-
228
- expect(extractAlgorithms(optionsNode)).toEqual(['RS256']);
229
- });
230
- });
231
-
232
- // ---------------------------------------------------------------------------
233
- // Layer 2 — no-hardcoded-secret: parser-unreachable guards inside
234
- // resolveConstLiteralValue(). A real ESLint context always exposes
235
- // sourceCode.getScope, and a scope-manager 'Variable' def always hangs off a
236
- // VariableDeclaration parent — these defensive branches need a mock context
237
- // and synthetic scope objects.
238
- // ---------------------------------------------------------------------------
239
- describe('coverage (unit): no-hardcoded-secret defensive scope guards', () => {
240
- const makeSignCall = (
241
- secretArg: Record<string, unknown>,
242
- ): TSESTree.CallExpression =>
243
- ({
244
- type: 'CallExpression',
245
- callee: {
246
- type: 'MemberExpression',
247
- object: { type: 'Identifier', name: 'jwt' },
248
- property: { type: 'Identifier', name: 'sign' },
249
- computed: false,
250
- },
251
- arguments: [{ type: 'ObjectExpression', properties: [] }, secretArg],
252
- }) as unknown as TSESTree.CallExpression;
253
-
254
- it('treats the secret as unresolvable when sourceCode.getScope is absent', () => {
255
- const { listeners, reports, context } = createWithMockContext(noHardcodedSecret);
256
- (context.sourceCode as { getScope?: unknown }).getScope = undefined;
257
-
258
- const secretId = { type: 'Identifier', name: 'SECRET' };
259
- (listeners.CallExpression as (n: TSESTree.CallExpression) => void)(
260
- makeSignCall(secretId),
261
- );
262
-
263
- // getScope?.() -> undefined -> ?? null -> early null: identifier cannot
264
- // be resolved to a literal, so it is treated as a safe key source.
265
- expect(reports).toHaveLength(0);
266
- });
267
-
268
- it('returns null when the resolved def has no parent declaration', () => {
269
- const { listeners, reports, context } = createWithMockContext(noHardcodedSecret);
270
- const secretId = { type: 'Identifier', name: 'SECRET' };
271
- const scope = {
272
- variables: [],
273
- childScopes: [],
274
- references: [
275
- {
276
- identifier: secretId,
277
- resolved: {
278
- defs: [{ type: 'Variable', parent: undefined, node: {} }],
279
- },
280
- },
281
- ],
282
- };
283
- (context.sourceCode as { getScope?: unknown }).getScope = () => scope;
284
-
285
- (listeners.CallExpression as (n: TSESTree.CallExpression) => void)(
286
- makeSignCall(secretId),
287
- );
288
-
289
- // decl?.type is undefined -> not a VariableDeclaration -> null -> safe.
290
- expect(reports).toHaveLength(0);
291
- });
292
-
293
- it('returns null when the def parent is not a VariableDeclaration', () => {
294
- const { listeners, reports, context } = createWithMockContext(noHardcodedSecret);
295
- const secretId = { type: 'Identifier', name: 'SECRET' };
296
- const scope = {
297
- variables: [],
298
- childScopes: [],
299
- references: [
300
- {
301
- identifier: secretId,
302
- resolved: {
303
- defs: [
304
- {
305
- type: 'Variable',
306
- parent: { type: 'ExpressionStatement' },
307
- node: { init: { type: 'Literal', value: 'x' } },
308
- },
309
- ],
310
- },
311
- },
312
- ],
313
- };
314
- (context.sourceCode as { getScope?: unknown }).getScope = () => scope;
315
-
316
- (listeners.CallExpression as (n: TSESTree.CallExpression) => void)(
317
- makeSignCall(secretId),
318
- );
319
-
320
- // parent.type mismatch -> null -> identifier treated as safe key source.
321
- expect(reports).toHaveLength(0);
322
- });
323
- });
@@ -1,107 +0,0 @@
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-plugin-jwt-security Type Exports
9
- *
10
- * Barrel file that exports all JWT security rule Options types.
11
- */
12
-
13
- import type { SecurityRuleOptions } from '@interlace/eslint-devkit';
14
-
15
- // Base options interface for JWT rules
16
- export interface JwtRuleOptions extends SecurityRuleOptions {
17
- /** JWT libraries to consider for detection. Default: all supported libraries */
18
- targetLibraries?: Array<
19
- | 'jsonwebtoken'
20
- | 'jose'
21
- | 'express-jwt'
22
- | '@nestjs/jwt'
23
- | 'jwks-rsa'
24
- | 'jwt-decode'
25
- >;
26
- }
27
-
28
- // Rule-specific options
29
- export interface NoAlgorithmNoneOptions extends JwtRuleOptions {
30
- /** Allow 'none' algorithm in test files. Default: false */
31
- allowInTests?: boolean;
32
- }
33
-
34
- export interface NoAlgorithmConfusionOptions extends JwtRuleOptions {
35
- /** Symmetric algorithms that should not be used with public keys */
36
- symmetricAlgorithms?: string[];
37
- }
38
-
39
- export interface RequireAlgorithmWhitelistOptions extends JwtRuleOptions {
40
- /** Recommended algorithms to suggest. Default: ['RS256', 'ES256'] */
41
- recommendedAlgorithms?: string[];
42
- }
43
-
44
- export interface NoDecodeWithoutVerifyOptions extends JwtRuleOptions {
45
- /** Allow decode() for header inspection before verify. Default: false */
46
- allowHeaderInspection?: boolean;
47
- }
48
-
49
- export interface NoWeakSecretOptions extends JwtRuleOptions {
50
- /** Minimum secret length in characters. Default: 32 (256 bits) */
51
- minSecretLength?: number;
52
- }
53
-
54
- export interface RequireExpirationOptions extends JwtRuleOptions {
55
- /** Maximum allowed expiration time in seconds. Default: 86400 (24 hours) */
56
- maxExpirationSeconds?: number;
57
- }
58
-
59
- export interface RequireIssuerValidationOptions extends JwtRuleOptions {
60
- /** Known valid issuers to suggest. Default: [] */
61
- knownIssuers?: string[];
62
- }
63
-
64
- export interface RequireAudienceValidationOptions extends JwtRuleOptions {
65
- /** Known valid audiences to suggest. Default: [] */
66
- knownAudiences?: string[];
67
- }
68
-
69
- export interface NoSensitivePayloadOptions extends JwtRuleOptions {
70
- /** Additional field names to flag as sensitive. Default: [] */
71
- additionalSensitiveFields?: string[];
72
- }
73
-
74
- export interface NoHardcodedSecretOptions extends JwtRuleOptions {
75
- /** Patterns that indicate environment variable usage (safe). Default: process.env */
76
- envPatterns?: string[];
77
- }
78
-
79
- export interface RequireJwksValidationOptions extends JwtRuleOptions {
80
- /** Require cache configuration. Default: true */
81
- requireCache?: boolean;
82
- /** Require rate limiting. Default: true */
83
- requireRateLimit?: boolean;
84
- }
85
-
86
- export interface NoInsecureKidOptions extends JwtRuleOptions {
87
- /** Allow custom KID validation functions. Default: [] */
88
- trustedKidValidators?: string[];
89
- }
90
-
91
- /**
92
- * Combined type for all JWT rule options
93
- */
94
- export type AllJwtRulesOptions = {
95
- 'no-algorithm-none'?: NoAlgorithmNoneOptions;
96
- 'no-algorithm-confusion'?: NoAlgorithmConfusionOptions;
97
- 'require-algorithm-whitelist'?: RequireAlgorithmWhitelistOptions;
98
- 'no-decode-without-verify'?: NoDecodeWithoutVerifyOptions;
99
- 'no-weak-secret'?: NoWeakSecretOptions;
100
- 'require-expiration'?: RequireExpirationOptions;
101
- 'require-issuer-validation'?: RequireIssuerValidationOptions;
102
- 'require-audience-validation'?: RequireAudienceValidationOptions;
103
- 'no-sensitive-payload'?: NoSensitivePayloadOptions;
104
- 'no-hardcoded-secret'?: NoHardcodedSecretOptions;
105
- 'require-jwks-validation'?: RequireJwksValidationOptions;
106
- 'no-insecure-kid'?: NoInsecureKidOptions;
107
- };