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.
- package/AGENTS.md +102 -0
- package/CHANGELOG.md +161 -0
- package/LICENSE +23 -0
- package/README.md +158 -0
- package/dist/AGENTS.md +102 -0
- package/dist/CHANGELOG.md +161 -0
- package/dist/LICENSE +23 -0
- package/dist/README.md +158 -0
- package/dist/package.json +125 -0
- package/dist/src/index.d.ts +101 -0
- package/dist/src/index.js +171 -0
- package/dist/src/oxlint.d.ts +20 -0
- package/dist/src/oxlint.js +21 -0
- package/dist/src/rules/no-algorithm-confusion/index.d.ts +24 -0
- package/dist/src/rules/no-algorithm-confusion/index.js +165 -0
- package/dist/src/rules/no-algorithm-none/index.d.ts +24 -0
- package/dist/src/rules/no-algorithm-none/index.js +222 -0
- package/dist/src/rules/no-decode-without-verify/index.d.ts +23 -0
- package/dist/src/rules/no-decode-without-verify/index.js +131 -0
- package/dist/src/rules/no-hardcoded-secret/index.d.ts +23 -0
- package/dist/src/rules/no-hardcoded-secret/index.js +189 -0
- package/dist/src/rules/no-sensitive-payload/index.d.ts +20 -0
- package/dist/src/rules/no-sensitive-payload/index.js +103 -0
- package/dist/src/rules/no-timestamp-manipulation/index.d.ts +23 -0
- package/dist/src/rules/no-timestamp-manipulation/index.js +97 -0
- package/dist/src/rules/no-weak-secret/index.d.ts +23 -0
- package/dist/src/rules/no-weak-secret/index.js +165 -0
- package/dist/src/rules/require-algorithm-whitelist/index.d.ts +24 -0
- package/dist/src/rules/require-algorithm-whitelist/index.js +113 -0
- package/dist/src/rules/require-audience-validation/index.d.ts +20 -0
- package/dist/src/rules/require-audience-validation/index.js +100 -0
- package/dist/src/rules/require-expiration/index.d.ts +23 -0
- package/dist/src/rules/require-expiration/index.js +136 -0
- package/dist/src/rules/require-issued-at/index.d.ts +23 -0
- package/dist/src/rules/require-issued-at/index.js +113 -0
- package/dist/src/rules/require-issuer-validation/index.d.ts +20 -0
- package/dist/src/rules/require-issuer-validation/index.js +100 -0
- package/dist/src/rules/require-max-age/index.d.ts +24 -0
- package/dist/src/rules/require-max-age/index.js +93 -0
- package/dist/src/types/index.d.ts +82 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/utils/index.d.ts +98 -0
- package/dist/src/utils/index.js +238 -0
- package/package.json +125 -0
- package/src/__compatibility__/jwt-interface.spec.ts +192 -0
- package/src/index.test.ts +47 -0
- package/src/index.ts +209 -0
- package/src/oxlint.test.ts +61 -0
- package/src/oxlint.ts +23 -0
- package/src/rules/no-algorithm-confusion/index.ts +208 -0
- package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +106 -0
- package/src/rules/no-algorithm-none/index.ts +270 -0
- package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +146 -0
- package/src/rules/no-decode-without-verify/index.ts +162 -0
- package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +76 -0
- package/src/rules/no-hardcoded-secret/index.ts +230 -0
- package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +97 -0
- package/src/rules/no-sensitive-payload/index.ts +123 -0
- package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +157 -0
- package/src/rules/no-timestamp-manipulation/index.ts +118 -0
- package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +54 -0
- package/src/rules/no-weak-secret/index.ts +204 -0
- package/src/rules/no-weak-secret/no-weak-secret.test.ts +114 -0
- package/src/rules/require-algorithm-whitelist/index.ts +145 -0
- package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +72 -0
- package/src/rules/require-audience-validation/index.ts +120 -0
- package/src/rules/require-audience-validation/require-audience-validation.test.ts +68 -0
- package/src/rules/require-expiration/index.ts +170 -0
- package/src/rules/require-expiration/require-expiration.test.ts +135 -0
- package/src/rules/require-issued-at/index.ts +143 -0
- package/src/rules/require-issued-at/require-issued-at.test.ts +64 -0
- package/src/rules/require-issuer-validation/index.ts +120 -0
- package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +68 -0
- package/src/rules/require-max-age/index.ts +117 -0
- package/src/rules/require-max-age/require-max-age.test.ts +69 -0
- package/src/tests/coverage-dual-layer.test.ts +323 -0
- package/src/types/index.ts +107 -0
- package/src/utils/index.ts +285 -0
- package/src/utils/utils.test.ts +324 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noWeakSecret = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const utils_1 = require("../../utils");
|
|
11
|
+
// Common weak secret patterns
|
|
12
|
+
const WEAK_SECRET_PATTERNS = [
|
|
13
|
+
/^secret$/i,
|
|
14
|
+
/^password$/i,
|
|
15
|
+
/^123456/,
|
|
16
|
+
/^test/i,
|
|
17
|
+
/^demo/i,
|
|
18
|
+
/^example/i,
|
|
19
|
+
/^changeme/i,
|
|
20
|
+
/^default/i,
|
|
21
|
+
];
|
|
22
|
+
exports.noWeakSecret = (0, eslint_devkit_1.createRule)({
|
|
23
|
+
name: 'no-weak-secret',
|
|
24
|
+
meta: {
|
|
25
|
+
type: 'problem',
|
|
26
|
+
docs: {
|
|
27
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-weak-secret.md',
|
|
28
|
+
description: 'Require strong secrets (256+ bits) for HMAC-based JWT signing',
|
|
29
|
+
cwe: 'CWE-326',
|
|
30
|
+
cvss: 5.9,
|
|
31
|
+
},
|
|
32
|
+
fixable: undefined,
|
|
33
|
+
hasSuggestions: false,
|
|
34
|
+
messages: {
|
|
35
|
+
weakSecret: (0, eslint_devkit_1.formatLLMMessage)({
|
|
36
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
37
|
+
issueName: 'Weak JWT Secret',
|
|
38
|
+
cwe: 'CWE-326',
|
|
39
|
+
description: 'JWT secret is too weak and can be brute-forced',
|
|
40
|
+
severity: 'HIGH',
|
|
41
|
+
fix: 'Use crypto.randomBytes(32).toString("hex") for secrets',
|
|
42
|
+
documentationLink: 'https://tools.ietf.org/html/rfc8725',
|
|
43
|
+
}),
|
|
44
|
+
shortSecret: (0, eslint_devkit_1.formatLLMMessage)({
|
|
45
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
46
|
+
issueName: 'Short JWT Secret',
|
|
47
|
+
cwe: 'CWE-326',
|
|
48
|
+
description: 'JWT secret must be at least 32 characters (256 bits) for HS256',
|
|
49
|
+
severity: 'HIGH',
|
|
50
|
+
fix: 'Generate a secret with: crypto.randomBytes(32).toString("hex")',
|
|
51
|
+
documentationLink: 'https://tools.ietf.org/html/rfc8725',
|
|
52
|
+
}),
|
|
53
|
+
useStrongSecret: (0, eslint_devkit_1.formatLLMMessage)({
|
|
54
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
55
|
+
issueName: 'Use Strong Secret',
|
|
56
|
+
description: 'Replace with cryptographically strong secret',
|
|
57
|
+
severity: 'LOW',
|
|
58
|
+
fix: 'Use process.env.JWT_SECRET with a 256-bit random value',
|
|
59
|
+
documentationLink: 'https://nodejs.org/api/crypto.html',
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
schema: [
|
|
63
|
+
{
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
minSecretLength: {
|
|
67
|
+
type: 'integer',
|
|
68
|
+
default: 32,
|
|
69
|
+
minimum: 16,
|
|
70
|
+
description: 'Minimum secret length in characters',
|
|
71
|
+
},
|
|
72
|
+
trustedSanitizers: {
|
|
73
|
+
type: 'array',
|
|
74
|
+
items: { type: 'string' },
|
|
75
|
+
default: [],
|
|
76
|
+
},
|
|
77
|
+
trustedAnnotations: {
|
|
78
|
+
type: 'array',
|
|
79
|
+
items: { type: 'string' },
|
|
80
|
+
default: [],
|
|
81
|
+
},
|
|
82
|
+
strictMode: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
default: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
defaultOptions: [
|
|
92
|
+
{
|
|
93
|
+
minSecretLength: 32,
|
|
94
|
+
trustedSanitizers: [],
|
|
95
|
+
trustedAnnotations: [],
|
|
96
|
+
strictMode: false,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
create(context) {
|
|
100
|
+
const options = context.options[0] ?? {};
|
|
101
|
+
const { minSecretLength = 32 } = options;
|
|
102
|
+
/**
|
|
103
|
+
* Check if a value matches known weak patterns
|
|
104
|
+
*/
|
|
105
|
+
const isKnownWeakPattern = (value) => {
|
|
106
|
+
return WEAK_SECRET_PATTERNS.some((pattern) => pattern.test(value));
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Check the secret argument for weakness
|
|
110
|
+
*/
|
|
111
|
+
const checkSecret = (secretNode) => {
|
|
112
|
+
// Environment variables are considered safe (configuration)
|
|
113
|
+
if ((0, utils_1.isEnvVariable)(secretNode)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Check string literals
|
|
117
|
+
if (secretNode.type === 'Literal' &&
|
|
118
|
+
typeof secretNode.value === 'string') {
|
|
119
|
+
const secretValue = secretNode.value;
|
|
120
|
+
// Check for known weak patterns
|
|
121
|
+
if (isKnownWeakPattern(secretValue)) {
|
|
122
|
+
context.report({
|
|
123
|
+
node: secretNode,
|
|
124
|
+
messageId: 'weakSecret',
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Check for short secrets
|
|
129
|
+
if (secretValue.length < minSecretLength) {
|
|
130
|
+
context.report({
|
|
131
|
+
node: secretNode,
|
|
132
|
+
messageId: 'shortSecret',
|
|
133
|
+
data: {
|
|
134
|
+
length: String(secretValue.length),
|
|
135
|
+
minLength: String(minSecretLength),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Template literals with obvious weak values
|
|
141
|
+
if (secretNode.type === 'TemplateLiteral' && secretNode.quasis.length === 1) {
|
|
142
|
+
const rawValue = secretNode.quasis[0].value.raw;
|
|
143
|
+
if (isKnownWeakPattern(rawValue) || rawValue.length < minSecretLength) {
|
|
144
|
+
context.report({
|
|
145
|
+
node: secretNode,
|
|
146
|
+
messageId: 'weakSecret',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
CallExpression(node) {
|
|
153
|
+
// Check both sign and verify operations
|
|
154
|
+
if (!(0, utils_1.isSignOperation)(node) && !(0, utils_1.isVerifyOperation)(node)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Secret is usually the second argument
|
|
158
|
+
if (node.arguments.length >= 2) {
|
|
159
|
+
checkSecret(node.arguments[1]);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
exports.default = exports.noWeakSecret;
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
* ESLint Rule: require-algorithm-whitelist
|
|
8
|
+
*
|
|
9
|
+
* Enforces explicit algorithm specification in JWT verify operations.
|
|
10
|
+
* Without explicit algorithms, the token's header algorithm is trusted,
|
|
11
|
+
* enabling algorithm substitution attacks.
|
|
12
|
+
*
|
|
13
|
+
* CWE-757: Selection of Less-Secure Algorithm During Negotiation
|
|
14
|
+
*
|
|
15
|
+
* @see https://tools.ietf.org/html/rfc8725
|
|
16
|
+
*/
|
|
17
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
18
|
+
import type { RequireAlgorithmWhitelistOptions } from '../../types';
|
|
19
|
+
type MessageIds = 'missingAlgorithmWhitelist' | 'addAlgorithmWhitelist';
|
|
20
|
+
type RuleOptions = [RequireAlgorithmWhitelistOptions?];
|
|
21
|
+
export declare const requireAlgorithmWhitelist: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
export default requireAlgorithmWhitelist;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.requireAlgorithmWhitelist = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const utils_1 = require("../../utils");
|
|
11
|
+
exports.requireAlgorithmWhitelist = (0, eslint_devkit_1.createRule)({
|
|
12
|
+
name: 'require-algorithm-whitelist',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-algorithm-whitelist.md',
|
|
17
|
+
description: 'Require explicit algorithm specification in JWT verify operations',
|
|
18
|
+
cwe: 'CWE-757',
|
|
19
|
+
cvss: 7.5,
|
|
20
|
+
},
|
|
21
|
+
fixable: undefined,
|
|
22
|
+
hasSuggestions: false,
|
|
23
|
+
messages: {
|
|
24
|
+
missingAlgorithmWhitelist: (0, eslint_devkit_1.formatLLMMessage)({
|
|
25
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
26
|
+
issueName: 'Missing Algorithm Whitelist',
|
|
27
|
+
cwe: 'CWE-757',
|
|
28
|
+
description: 'JWT verification without explicit algorithms trusts the token header',
|
|
29
|
+
severity: 'HIGH',
|
|
30
|
+
fix: 'Add algorithms: ["RS256"] or algorithms: ["ES256"] to options',
|
|
31
|
+
documentationLink: 'https://tools.ietf.org/html/rfc8725',
|
|
32
|
+
}),
|
|
33
|
+
addAlgorithmWhitelist: (0, eslint_devkit_1.formatLLMMessage)({
|
|
34
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
35
|
+
issueName: 'Add Algorithm Whitelist',
|
|
36
|
+
description: 'Specify allowed algorithms explicitly',
|
|
37
|
+
severity: 'LOW',
|
|
38
|
+
fix: 'jwt.verify(token, key, { algorithms: ["RS256"] })',
|
|
39
|
+
documentationLink: 'https://tools.ietf.org/html/rfc8725',
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
schema: [
|
|
43
|
+
{
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
recommendedAlgorithms: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
items: { type: 'string' },
|
|
49
|
+
default: ['RS256', 'ES256'],
|
|
50
|
+
description: 'Algorithms to suggest in auto-fix',
|
|
51
|
+
},
|
|
52
|
+
trustedSanitizers: {
|
|
53
|
+
type: 'array',
|
|
54
|
+
items: { type: 'string' },
|
|
55
|
+
default: [],
|
|
56
|
+
},
|
|
57
|
+
trustedAnnotations: {
|
|
58
|
+
type: 'array',
|
|
59
|
+
items: { type: 'string' },
|
|
60
|
+
default: [],
|
|
61
|
+
},
|
|
62
|
+
strictMode: {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
default: false,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
defaultOptions: [
|
|
72
|
+
{
|
|
73
|
+
recommendedAlgorithms: ['RS256', 'ES256'],
|
|
74
|
+
trustedSanitizers: [],
|
|
75
|
+
trustedAnnotations: [],
|
|
76
|
+
strictMode: false,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
create(context) {
|
|
80
|
+
return {
|
|
81
|
+
CallExpression(node) {
|
|
82
|
+
// Only check verify operations
|
|
83
|
+
if (!(0, utils_1.isVerifyOperation)(node)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Need at least token and key arguments
|
|
87
|
+
if (node.arguments.length < 2) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
91
|
+
// No options at all - definitely missing algorithms
|
|
92
|
+
if (!optionsArg) {
|
|
93
|
+
context.report({
|
|
94
|
+
node,
|
|
95
|
+
messageId: 'missingAlgorithmWhitelist',
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Options exist but no algorithms property
|
|
100
|
+
const hasAlgorithms = (0, utils_1.hasOption)(optionsArg, 'algorithms') ||
|
|
101
|
+
(0, utils_1.hasOption)(optionsArg, 'algorithm') ||
|
|
102
|
+
(0, utils_1.hasOption)(optionsArg, 'alg');
|
|
103
|
+
if (!hasAlgorithms) {
|
|
104
|
+
context.report({
|
|
105
|
+
node: optionsArg,
|
|
106
|
+
messageId: 'missingAlgorithmWhitelist',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
exports.default = exports.requireAlgorithmWhitelist;
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
* ESLint Rule: require-audience-validation
|
|
8
|
+
*
|
|
9
|
+
* Mandates audience (aud) claim validation in JWT verify operations.
|
|
10
|
+
* Without audience validation, tokens intended for other services are accepted.
|
|
11
|
+
*
|
|
12
|
+
* CWE-287: Improper Authentication
|
|
13
|
+
*/
|
|
14
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
15
|
+
import type { RequireAudienceValidationOptions } from '../../types';
|
|
16
|
+
type RuleOptions = [RequireAudienceValidationOptions?];
|
|
17
|
+
export declare const requireAudienceValidation: TSESLint.RuleModule<"missingAudienceValidation", RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
export default requireAudienceValidation;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.requireAudienceValidation = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const utils_1 = require("../../utils");
|
|
11
|
+
exports.requireAudienceValidation = (0, eslint_devkit_1.createRule)({
|
|
12
|
+
name: 'require-audience-validation',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-audience-validation.md',
|
|
17
|
+
description: 'Require audience (aud) claim validation in JWT verify operations',
|
|
18
|
+
cwe: 'CWE-287',
|
|
19
|
+
cvss: 9.8,
|
|
20
|
+
},
|
|
21
|
+
fixable: undefined,
|
|
22
|
+
hasSuggestions: false,
|
|
23
|
+
messages: {
|
|
24
|
+
missingAudienceValidation: (0, eslint_devkit_1.formatLLMMessage)({
|
|
25
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
26
|
+
issueName: 'Missing Audience Validation',
|
|
27
|
+
cwe: 'CWE-287',
|
|
28
|
+
description: 'JWT verification without audience validation accepts tokens intended for other services',
|
|
29
|
+
severity: 'MEDIUM',
|
|
30
|
+
fix: 'Add audience option: { audience: "https://api.example.com" }',
|
|
31
|
+
documentationLink: 'https://tools.ietf.org/html/rfc8725',
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
schema: [
|
|
35
|
+
{
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
knownAudiences: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: { type: 'string' },
|
|
41
|
+
default: [],
|
|
42
|
+
description: 'Known valid audiences to suggest',
|
|
43
|
+
},
|
|
44
|
+
trustedSanitizers: {
|
|
45
|
+
type: 'array',
|
|
46
|
+
items: { type: 'string' },
|
|
47
|
+
default: [],
|
|
48
|
+
},
|
|
49
|
+
trustedAnnotations: {
|
|
50
|
+
type: 'array',
|
|
51
|
+
items: { type: 'string' },
|
|
52
|
+
default: [],
|
|
53
|
+
},
|
|
54
|
+
strictMode: {
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
default: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
defaultOptions: [
|
|
64
|
+
{
|
|
65
|
+
knownAudiences: [],
|
|
66
|
+
trustedSanitizers: [],
|
|
67
|
+
trustedAnnotations: [],
|
|
68
|
+
strictMode: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
create(context) {
|
|
72
|
+
return {
|
|
73
|
+
CallExpression(node) {
|
|
74
|
+
if (!(0, utils_1.isVerifyOperation)(node)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (node.arguments.length < 2) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
81
|
+
// No options at all
|
|
82
|
+
if (!optionsArg) {
|
|
83
|
+
context.report({
|
|
84
|
+
node,
|
|
85
|
+
messageId: 'missingAudienceValidation',
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Check for audience option
|
|
90
|
+
if (!(0, utils_1.hasOption)(optionsArg, 'audience') && !(0, utils_1.hasOption)(optionsArg, 'aud')) {
|
|
91
|
+
context.report({
|
|
92
|
+
node: optionsArg,
|
|
93
|
+
messageId: 'missingAudienceValidation',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
exports.default = exports.requireAudienceValidation;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
* ESLint Rule: require-expiration
|
|
8
|
+
*
|
|
9
|
+
* Mandates expiration claim (exp) or expiresIn option in JWT sign operations.
|
|
10
|
+
* Tokens without expiration are valid forever, creating security risks.
|
|
11
|
+
*
|
|
12
|
+
* CWE-613: Insufficient Session Expiration
|
|
13
|
+
*
|
|
14
|
+
* @see https://tools.ietf.org/html/rfc8725
|
|
15
|
+
*/
|
|
16
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
17
|
+
import type { RequireExpirationOptions } from '../../types';
|
|
18
|
+
type MessageIds = 'missingExpiration' | 'addExpiration';
|
|
19
|
+
type RuleOptions = [RequireExpirationOptions?];
|
|
20
|
+
export declare const requireExpiration: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
export default requireExpiration;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.requireExpiration = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const utils_1 = require("../../utils");
|
|
11
|
+
exports.requireExpiration = (0, eslint_devkit_1.createRule)({
|
|
12
|
+
name: 'require-expiration',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-expiration.md',
|
|
17
|
+
description: 'Require expiration claim (exp) or expiresIn option in JWT signing',
|
|
18
|
+
cwe: 'CWE-613',
|
|
19
|
+
cvss: 5.4,
|
|
20
|
+
},
|
|
21
|
+
hasSuggestions: true,
|
|
22
|
+
messages: {
|
|
23
|
+
missingExpiration: (0, eslint_devkit_1.formatLLMMessage)({
|
|
24
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
25
|
+
issueName: 'Missing JWT Expiration',
|
|
26
|
+
cwe: 'CWE-613',
|
|
27
|
+
description: 'JWT without expiration is valid forever, increasing exposure window',
|
|
28
|
+
severity: 'MEDIUM',
|
|
29
|
+
fix: 'Add expiresIn: "1h" or exp claim to payload',
|
|
30
|
+
documentationLink: 'https://tools.ietf.org/html/rfc8725',
|
|
31
|
+
}),
|
|
32
|
+
addExpiration: (0, eslint_devkit_1.formatLLMMessage)({
|
|
33
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
34
|
+
issueName: 'Add Expiration',
|
|
35
|
+
description: 'Add expiration to limit token lifetime',
|
|
36
|
+
severity: 'LOW',
|
|
37
|
+
fix: 'jwt.sign(payload, secret, { expiresIn: "1h" })',
|
|
38
|
+
documentationLink: 'https://www.npmjs.com/package/jsonwebtoken',
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
schema: [
|
|
42
|
+
{
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
maxExpirationSeconds: {
|
|
46
|
+
type: 'integer',
|
|
47
|
+
default: 86400,
|
|
48
|
+
description: 'Maximum allowed expiration time in seconds (24h default)',
|
|
49
|
+
},
|
|
50
|
+
trustedSanitizers: {
|
|
51
|
+
type: 'array',
|
|
52
|
+
items: { type: 'string' },
|
|
53
|
+
default: [],
|
|
54
|
+
},
|
|
55
|
+
trustedAnnotations: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
items: { type: 'string' },
|
|
58
|
+
default: [],
|
|
59
|
+
},
|
|
60
|
+
strictMode: {
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
default: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
defaultOptions: [
|
|
70
|
+
{
|
|
71
|
+
maxExpirationSeconds: 86400,
|
|
72
|
+
trustedSanitizers: [],
|
|
73
|
+
trustedAnnotations: [],
|
|
74
|
+
strictMode: false,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
create(context) {
|
|
78
|
+
/**
|
|
79
|
+
* Check if payload contains exp claim
|
|
80
|
+
*/
|
|
81
|
+
// oxlint-disable-next-line consistent-function-scoping
|
|
82
|
+
const payloadHasExp = (payloadNode) => {
|
|
83
|
+
if (payloadNode.type !== 'ObjectExpression') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return payloadNode.properties.some((prop) => prop.type === 'Property' &&
|
|
87
|
+
prop.key.type === 'Identifier' &&
|
|
88
|
+
prop.key.name === 'exp');
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
CallExpression(node) {
|
|
92
|
+
// Only check sign operations
|
|
93
|
+
if (!(0, utils_1.isSignOperation)(node)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Need at least payload argument
|
|
97
|
+
if (node.arguments.length < 1) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const payloadArg = node.arguments[0];
|
|
101
|
+
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
102
|
+
// Check if payload has exp claim
|
|
103
|
+
if (payloadHasExp(payloadArg)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Check if options has expiresIn
|
|
107
|
+
if (optionsArg && (0, utils_1.hasOption)(optionsArg, 'expiresIn')) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Report missing expiration — suggest adding expiresIn to the options object
|
|
111
|
+
const optionsArg3 = node.arguments[2];
|
|
112
|
+
context.report({
|
|
113
|
+
node,
|
|
114
|
+
messageId: 'missingExpiration',
|
|
115
|
+
suggest: [
|
|
116
|
+
{
|
|
117
|
+
messageId: 'addExpiration',
|
|
118
|
+
fix(fixer) {
|
|
119
|
+
const sourceCode = context.sourceCode;
|
|
120
|
+
if (optionsArg3 && optionsArg3.type === eslint_devkit_1.AST_NODE_TYPES.ObjectExpression) {
|
|
121
|
+
// Options object exists but lacks expiresIn — insert property
|
|
122
|
+
const openBrace = sourceCode.getFirstToken(optionsArg3);
|
|
123
|
+
return fixer.insertTextAfter(openBrace, ' expiresIn: \'1h\',');
|
|
124
|
+
}
|
|
125
|
+
// No options arg — insert { expiresIn: '1h' } as third argument
|
|
126
|
+
const lastArg = node.arguments[node.arguments.length - 1];
|
|
127
|
+
return fixer.insertTextAfter(lastArg, ', { expiresIn: \'1h\' }');
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
exports.default = exports.requireExpiration;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
* ESLint Rule: require-issued-at
|
|
8
|
+
*
|
|
9
|
+
* Mandates the `iat` (issued at) claim in JWT tokens for freshness validation.
|
|
10
|
+
* Without `iat`, tokens cannot be validated for age, enabling the
|
|
11
|
+
* "Back to the Future" replay attack (LightSEC 2025).
|
|
12
|
+
*
|
|
13
|
+
* CWE-294: Authentication Bypass by Capture-replay
|
|
14
|
+
*
|
|
15
|
+
* @see https://securitypattern.com/post/jwt-back-to-the-future
|
|
16
|
+
*/
|
|
17
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
18
|
+
import type { JwtRuleOptions } from '../../types';
|
|
19
|
+
type RuleOptions = [JwtRuleOptions?];
|
|
20
|
+
export declare const requireIssuedAt: TSESLint.RuleModule<"missingIssuedAt", RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
export default requireIssuedAt;
|