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-audience-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 { requireAudienceValidation } 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-audience-validation', () => {
23
- describe('Valid Code', () => {
24
- ruleTester.run('valid - with audience', requireAudienceValidation, {
25
- valid: [
26
- { code: `jwt.verify(token, secret, { audience: 'https://api.example.com' });` },
27
- { code: `jwt.verify(token, secret, { aud: 'api.example.com' });` },
28
- { code: `jwt.verify(token, secret, { audience: ['api', 'web'] });` },
29
- { code: `jwt.sign(payload, secret);` }, // sign not checked
30
- // Only one argument - edge case
31
- { code: `jwt.verify(token);` },
32
- // jwtVerify with audience
33
- { code: `jwtVerify(token, key, { audience: 'https://api.example.com' });` },
34
- ],
35
- invalid: [],
36
- });
37
- });
38
-
39
- describe('Invalid Code', () => {
40
- ruleTester.run('invalid - missing audience', requireAudienceValidation, {
41
- valid: [],
42
- invalid: [
43
- {
44
- code: `jwt.verify(token, secret);`,
45
- errors: [{ messageId: 'missingAudienceValidation' }],
46
- },
47
- {
48
- code: `jwt.verify(token, secret, {});`,
49
- errors: [{ messageId: 'missingAudienceValidation' }],
50
- },
51
- {
52
- code: `jwt.verify(token, secret, { issuer: 'auth.example.com' });`,
53
- errors: [{ messageId: 'missingAudienceValidation' }],
54
- },
55
- // jwtVerify without audience
56
- {
57
- code: `jwtVerify(token, key);`,
58
- errors: [{ messageId: 'missingAudienceValidation' }],
59
- },
60
- // jwtVerify with only issuer
61
- {
62
- code: `jwtVerify(token, key, { issuer: 'auth.example.com' });`,
63
- errors: [{ messageId: 'missingAudienceValidation' }],
64
- },
65
- ],
66
- });
67
- });
68
- });
@@ -1,170 +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-expiration
9
- *
10
- * Mandates expiration claim (exp) or expiresIn option in JWT sign operations.
11
- * Tokens without expiration are valid forever, creating security risks.
12
- *
13
- * CWE-613: Insufficient Session Expiration
14
- *
15
- * @see https://tools.ietf.org/html/rfc8725
16
- */
17
- import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
18
- import { AST_NODE_TYPES, createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
19
- import {
20
- isSignOperation,
21
- getOptionsArgument,
22
- hasOption,
23
- } from '../../utils';
24
- import type { RequireExpirationOptions } from '../../types';
25
-
26
- type MessageIds =
27
- | 'missingExpiration'
28
- | 'addExpiration';
29
-
30
- type RuleOptions = [RequireExpirationOptions?];
31
-
32
- export const requireExpiration = createRule<RuleOptions, MessageIds>({
33
- name: 'require-expiration',
34
- meta: {
35
- type: 'problem',
36
- docs: {
37
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-expiration.md',
38
- description:
39
- 'Require expiration claim (exp) or expiresIn option in JWT signing',
40
- cwe: 'CWE-613',
41
- cvss: 5.4,
42
- },
43
- hasSuggestions: true,
44
- messages: {
45
- missingExpiration: formatLLMMessage({
46
- icon: MessageIcons.SECURITY,
47
- issueName: 'Missing JWT Expiration',
48
- cwe: 'CWE-613',
49
- description:
50
- 'JWT without expiration is valid forever, increasing exposure window',
51
- severity: 'MEDIUM',
52
- fix: 'Add expiresIn: "1h" or exp claim to payload',
53
- documentationLink: 'https://tools.ietf.org/html/rfc8725',
54
- }),
55
- addExpiration: formatLLMMessage({
56
- icon: MessageIcons.INFO,
57
- issueName: 'Add Expiration',
58
- description: 'Add expiration to limit token lifetime',
59
- severity: 'LOW',
60
- fix: 'jwt.sign(payload, secret, { expiresIn: "1h" })',
61
- documentationLink: 'https://www.npmjs.com/package/jsonwebtoken',
62
- }),
63
- },
64
- schema: [
65
- {
66
- type: 'object',
67
- properties: {
68
- maxExpirationSeconds: {
69
- type: 'integer',
70
- default: 86400,
71
- description: 'Maximum allowed expiration time in seconds (24h default)',
72
- },
73
- trustedSanitizers: {
74
- type: 'array',
75
- items: { type: 'string' },
76
- default: [],
77
- },
78
- trustedAnnotations: {
79
- type: 'array',
80
- items: { type: 'string' },
81
- default: [],
82
- },
83
- strictMode: {
84
- type: 'boolean',
85
- default: false,
86
- },
87
- },
88
- additionalProperties: false,
89
- },
90
- ],
91
- },
92
- defaultOptions: [
93
- {
94
- maxExpirationSeconds: 86400,
95
- trustedSanitizers: [],
96
- trustedAnnotations: [],
97
- strictMode: false,
98
- },
99
- ],
100
- create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
101
- /**
102
- * Check if payload contains exp claim
103
- */
104
- // oxlint-disable-next-line consistent-function-scoping
105
- const payloadHasExp = (payloadNode: TSESTree.Node): boolean => {
106
- if (payloadNode.type !== 'ObjectExpression') {
107
- return false;
108
- }
109
-
110
- return payloadNode.properties.some(
111
- (prop) =>
112
- prop.type === 'Property' &&
113
- prop.key.type === 'Identifier' &&
114
- prop.key.name === 'exp'
115
- );
116
- };
117
-
118
- return {
119
- CallExpression(node: TSESTree.CallExpression) {
120
- // Only check sign operations
121
- if (!isSignOperation(node)) {
122
- return;
123
- }
124
-
125
- // Need at least payload argument
126
- if (node.arguments.length < 1) {
127
- return;
128
- }
129
-
130
- const payloadArg = node.arguments[0];
131
- const optionsArg = getOptionsArgument(node, 2);
132
-
133
- // Check if payload has exp claim
134
- if (payloadHasExp(payloadArg)) {
135
- return;
136
- }
137
-
138
- // Check if options has expiresIn
139
- if (optionsArg && hasOption(optionsArg, 'expiresIn')) {
140
- return;
141
- }
142
-
143
- // Report missing expiration — suggest adding expiresIn to the options object
144
- const optionsArg3 = node.arguments[2];
145
- context.report({
146
- node,
147
- messageId: 'missingExpiration',
148
- suggest: [
149
- {
150
- messageId: 'addExpiration',
151
- fix(fixer) {
152
- const sourceCode = context.sourceCode;
153
- if (optionsArg3 && optionsArg3.type === AST_NODE_TYPES.ObjectExpression) {
154
- // Options object exists but lacks expiresIn — insert property
155
- const openBrace = sourceCode.getFirstToken(optionsArg3)!;
156
- return fixer.insertTextAfter(openBrace, ' expiresIn: \'1h\',');
157
- }
158
- // No options arg — insert { expiresIn: '1h' } as third argument
159
- const lastArg = node.arguments[node.arguments.length - 1];
160
- return fixer.insertTextAfter(lastArg, ', { expiresIn: \'1h\' }');
161
- },
162
- },
163
- ],
164
- });
165
- },
166
- };
167
- },
168
- });
169
-
170
- export default requireExpiration;
@@ -1,135 +0,0 @@
1
- /**
2
- * Tests for require-expiration 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 { requireExpiration } 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-expiration', () => {
23
- describe('Valid Code', () => {
24
- ruleTester.run('valid - with expiration', requireExpiration, {
25
- valid: [
26
- // expiresIn option
27
- { code: `jwt.sign(payload, secret, { expiresIn: '1h' });` },
28
- { code: `jwt.sign(payload, secret, { expiresIn: 3600 });` },
29
- // exp in payload
30
- { code: `jwt.sign({ sub: 'user', exp: 1234567890 }, secret);` },
31
- { code: `jwt.sign({ exp: Math.floor(Date.now()/1000) + 3600 }, secret);` },
32
- // verify not checked
33
- { code: `jwt.verify(token, secret);` },
34
- // signJWT with expiresIn
35
- { code: `signJWT(payload, key, { expiresIn: '1h' });` },
36
- // Zero arguments - edge case (line 118 coverage)
37
- { code: `jwt.sign();` },
38
- ],
39
- invalid: [],
40
- });
41
- });
42
-
43
- describe('Invalid Code', () => {
44
- ruleTester.run('invalid - no expiration', requireExpiration, {
45
- valid: [],
46
- invalid: [
47
- {
48
- code: `jwt.sign(payload, secret);`,
49
- errors: [
50
- {
51
- messageId: 'missingExpiration',
52
- suggestions: [
53
- {
54
- messageId: 'addExpiration',
55
- output: `jwt.sign(payload, secret, { expiresIn: '1h' });`,
56
- },
57
- ],
58
- },
59
- ],
60
- },
61
- {
62
- code: `jwt.sign(payload, secret, {});`,
63
- errors: [
64
- {
65
- messageId: 'missingExpiration',
66
- suggestions: [
67
- {
68
- messageId: 'addExpiration',
69
- output: `jwt.sign(payload, secret, { expiresIn: '1h',});`,
70
- },
71
- ],
72
- },
73
- ],
74
- },
75
- {
76
- code: `jwt.sign(payload, secret, { algorithm: 'RS256' });`,
77
- errors: [
78
- {
79
- messageId: 'missingExpiration',
80
- suggestions: [
81
- {
82
- messageId: 'addExpiration',
83
- output: `jwt.sign(payload, secret, { expiresIn: '1h', algorithm: 'RS256' });`,
84
- },
85
- ],
86
- },
87
- ],
88
- },
89
- {
90
- code: `jwt.sign({ sub: 'user', iat: Date.now() }, secret);`,
91
- errors: [
92
- {
93
- messageId: 'missingExpiration',
94
- suggestions: [
95
- {
96
- messageId: 'addExpiration',
97
- output: `jwt.sign({ sub: 'user', iat: Date.now() }, secret, { expiresIn: '1h' });`,
98
- },
99
- ],
100
- },
101
- ],
102
- },
103
- {
104
- code: `sign(payload, key);`,
105
- errors: [
106
- {
107
- messageId: 'missingExpiration',
108
- suggestions: [
109
- {
110
- messageId: 'addExpiration',
111
- output: `sign(payload, key, { expiresIn: '1h' });`,
112
- },
113
- ],
114
- },
115
- ],
116
- },
117
- // signJWT without expiration
118
- {
119
- code: `signJWT({ sub: 'user' }, key, { algorithm: 'RS256' });`,
120
- errors: [
121
- {
122
- messageId: 'missingExpiration',
123
- suggestions: [
124
- {
125
- messageId: 'addExpiration',
126
- output: `signJWT({ sub: 'user' }, key, { expiresIn: '1h', algorithm: 'RS256' });`,
127
- },
128
- ],
129
- },
130
- ],
131
- },
132
- ],
133
- });
134
- });
135
- });
@@ -1,143 +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-issued-at
9
- *
10
- * Mandates the `iat` (issued at) claim in JWT tokens for freshness validation.
11
- * Without `iat`, tokens cannot be validated for age, enabling the
12
- * "Back to the Future" replay attack (LightSEC 2025).
13
- *
14
- * CWE-294: Authentication Bypass by Capture-replay
15
- *
16
- * @see https://securitypattern.com/post/jwt-back-to-the-future
17
- */
18
- import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
19
- import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
20
- import { isSignOperation, getOptionsArgument, hasOption } from '../../utils';
21
- import type { JwtRuleOptions } from '../../types';
22
-
23
- type MessageIds = 'missingIssuedAt';
24
-
25
- type RuleOptions = [JwtRuleOptions?];
26
-
27
- export const requireIssuedAt = createRule<RuleOptions, MessageIds>({
28
- name: 'require-issued-at',
29
- meta: {
30
- type: 'problem',
31
- docs: {
32
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-issued-at.md',
33
- description:
34
- 'Require iat (issued at) claim for token freshness validation',
35
- cwe: 'CWE-294',
36
- cvss: 5,
37
- },
38
- fixable: undefined,
39
- hasSuggestions: false,
40
- messages: {
41
- missingIssuedAt: formatLLMMessage({
42
- icon: MessageIcons.SECURITY,
43
- issueName: 'Missing Issued At Claim',
44
- cwe: 'CWE-294',
45
- description:
46
- 'JWT without iat claim cannot be validated for freshness, enabling replay attacks',
47
- severity: 'MEDIUM',
48
- fix: 'Add iat claim to payload or use library option that adds it automatically',
49
- documentationLink: 'https://securitypattern.com/post/jwt-back-to-the-future',
50
- }),
51
- },
52
- schema: [
53
- {
54
- type: 'object',
55
- properties: {
56
- trustedSanitizers: {
57
- type: 'array',
58
- items: { type: 'string' },
59
- default: [],
60
- },
61
- trustedAnnotations: {
62
- type: 'array',
63
- items: { type: 'string' },
64
- default: [],
65
- },
66
- strictMode: {
67
- type: 'boolean',
68
- default: false,
69
- },
70
- },
71
- additionalProperties: false,
72
- },
73
- ],
74
- },
75
- defaultOptions: [
76
- {
77
- trustedSanitizers: [],
78
- trustedAnnotations: [],
79
- strictMode: false,
80
- },
81
- ],
82
- create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
83
- /**
84
- * Check if payload contains iat claim
85
- */
86
- // oxlint-disable-next-line consistent-function-scoping
87
- const payloadHasIat = (payloadNode: TSESTree.Node): boolean => {
88
- if (payloadNode.type !== 'ObjectExpression') {
89
- return false;
90
- }
91
-
92
- return payloadNode.properties.some(
93
- (prop) =>
94
- prop.type === 'Property' &&
95
- prop.key.type === 'Identifier' &&
96
- prop.key.name === 'iat'
97
- );
98
- };
99
-
100
- return {
101
- CallExpression(node: TSESTree.CallExpression) {
102
- // Only check sign operations
103
- if (!isSignOperation(node)) {
104
- return;
105
- }
106
-
107
- // Need at least payload argument
108
- if (node.arguments.length < 1) {
109
- return;
110
- }
111
-
112
- const payloadArg = node.arguments[0];
113
- const optionsArg = getOptionsArgument(node, 2);
114
-
115
- // Check if payload has iat claim
116
- if (payloadHasIat(payloadArg)) {
117
- return;
118
- }
119
-
120
- // Check if options has noTimestamp: false (meaning iat IS added)
121
- // jsonwebtoken adds iat by default unless noTimestamp: true
122
- if (optionsArg && hasOption(optionsArg, 'noTimestamp')) {
123
- // If noTimestamp is explicitly set, we flag it (they're disabling iat)
124
- context.report({
125
- node: optionsArg,
126
- messageId: 'missingIssuedAt',
127
- });
128
- return;
129
- }
130
-
131
- // For explicit payload objects without iat we intentionally do not
132
- // report — jsonwebtoken adds iat by default. (A dead empty `if` that
133
- // re-checked !payloadHasIat(payloadArg) — always true after the early
134
- // return above — was removed as unreachable-by-construction.)
135
-
136
- // Note: jsonwebtoken adds iat by default, so we don't flag by default
137
- // This rule primarily catches explicit noTimestamp: true usage
138
- },
139
- };
140
- },
141
- });
142
-
143
- export default requireIssuedAt;
@@ -1,64 +0,0 @@
1
- /**
2
- * Tests for require-issued-at 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 { requireIssuedAt } 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-issued-at', () => {
23
- describe('Valid Code', () => {
24
- ruleTester.run('valid - with iat', requireIssuedAt, {
25
- valid: [
26
- // iat in payload
27
- { code: `jwt.sign({ sub: 'user', iat: Date.now() }, secret);` },
28
- // Default behavior (jsonwebtoken adds iat automatically)
29
- { code: `jwt.sign(payload, secret);` },
30
- { code: `jwt.sign(payload, secret, { expiresIn: '1h' });` },
31
- // Verify not checked
32
- { code: `jwt.verify(token, secret);` },
33
- // No arguments - edge case
34
- { code: `sign();` },
35
- // Variable payload with iat
36
- { code: `jwt.sign({ iat: Math.floor(Date.now() / 1000), sub: 'user' }, secret);` },
37
- ],
38
- invalid: [],
39
- });
40
- });
41
-
42
- describe('Invalid Code', () => {
43
- ruleTester.run('invalid - noTimestamp disables iat', requireIssuedAt, {
44
- valid: [],
45
- invalid: [
46
- // noTimestamp: true explicitly disables iat
47
- {
48
- code: `jwt.sign({ sub: 'user' }, secret, { noTimestamp: true });`,
49
- errors: [{ messageId: 'missingIssuedAt' }],
50
- },
51
- // noTimestamp with other options
52
- {
53
- code: `jwt.sign(payload, secret, { expiresIn: '1h', noTimestamp: true });`,
54
- errors: [{ messageId: 'missingIssuedAt' }],
55
- },
56
- // SignJWT with noTimestamp
57
- {
58
- code: `signJWT(payload, key, { noTimestamp: true });`,
59
- errors: [{ messageId: 'missingIssuedAt' }],
60
- },
61
- ],
62
- });
63
- });
64
- });
@@ -1,120 +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-issuer-validation
9
- *
10
- * Mandates issuer (iss) claim validation in JWT verify operations.
11
- * Without issuer validation, tokens from any issuer are accepted.
12
- *
13
- * CWE-287: Improper Authentication
14
- */
15
- import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
16
- import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
17
- import { isVerifyOperation, getOptionsArgument, hasOption } from '../../utils';
18
- import type { RequireIssuerValidationOptions } from '../../types';
19
-
20
- type MessageIds = 'missingIssuerValidation';
21
-
22
- type RuleOptions = [RequireIssuerValidationOptions?];
23
-
24
- export const requireIssuerValidation = createRule<RuleOptions, MessageIds>({
25
- name: 'require-issuer-validation',
26
- meta: {
27
- type: 'problem',
28
- docs: {
29
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-issuer-validation.md',
30
- description:
31
- 'Require issuer (iss) claim validation in JWT verify operations',
32
- cwe: 'CWE-287',
33
- cvss: 9.8,
34
- },
35
- fixable: undefined,
36
- hasSuggestions: false,
37
- messages: {
38
- missingIssuerValidation: formatLLMMessage({
39
- icon: MessageIcons.SECURITY,
40
- issueName: 'Missing Issuer Validation',
41
- cwe: 'CWE-287',
42
- description:
43
- 'JWT verification without issuer validation accepts tokens from any source',
44
- severity: 'MEDIUM',
45
- fix: 'Add issuer option: { issuer: "https://auth.example.com" }',
46
- documentationLink: 'https://tools.ietf.org/html/rfc8725',
47
- }),
48
- },
49
- schema: [
50
- {
51
- type: 'object',
52
- properties: {
53
- knownIssuers: {
54
- type: 'array',
55
- items: { type: 'string' },
56
- default: [],
57
- description: 'Known valid issuers to suggest',
58
- },
59
- trustedSanitizers: {
60
- type: 'array',
61
- items: { type: 'string' },
62
- default: [],
63
- },
64
- trustedAnnotations: {
65
- type: 'array',
66
- items: { type: 'string' },
67
- default: [],
68
- },
69
- strictMode: {
70
- type: 'boolean',
71
- default: false,
72
- },
73
- },
74
- additionalProperties: false,
75
- },
76
- ],
77
- },
78
- defaultOptions: [
79
- {
80
- knownIssuers: [],
81
- trustedSanitizers: [],
82
- trustedAnnotations: [],
83
- strictMode: false,
84
- },
85
- ],
86
- create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
87
- return {
88
- CallExpression(node: TSESTree.CallExpression) {
89
- if (!isVerifyOperation(node)) {
90
- return;
91
- }
92
-
93
- if (node.arguments.length < 2) {
94
- return;
95
- }
96
-
97
- const optionsArg = getOptionsArgument(node, 2);
98
-
99
- // No options at all
100
- if (!optionsArg) {
101
- context.report({
102
- node,
103
- messageId: 'missingIssuerValidation',
104
- });
105
- return;
106
- }
107
-
108
- // Check for issuer option
109
- if (!hasOption(optionsArg, 'issuer') && !hasOption(optionsArg, 'iss')) {
110
- context.report({
111
- node: optionsArg,
112
- messageId: 'missingIssuerValidation',
113
- });
114
- }
115
- },
116
- };
117
- },
118
- });
119
-
120
- export default requireIssuerValidation;