eslint-plugin-jwt-security 2.3.0 → 2.3.1
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/README.md +5 -5
- package/package.json +16 -24
- package/src/index.d.ts +17 -0
- package/{dist/src → src}/index.js +16 -69
- package/src/oxlint.d.ts +17 -0
- package/src/oxlint.js +3 -0
- package/{dist/src → src}/rules/no-algorithm-confusion/index.js +0 -20
- package/{dist/src → src}/rules/no-algorithm-none/index.js +0 -38
- package/{dist/src → src}/rules/no-decode-without-verify/index.js +0 -14
- package/{dist/src → src}/rules/no-hardcoded-secret/index.js +0 -39
- package/{dist/src → src}/rules/no-sensitive-payload/index.js +0 -6
- package/{dist/src → src}/rules/no-timestamp-manipulation/index.js +0 -6
- package/{dist/src → src}/rules/no-weak-secret/index.js +0 -19
- package/{dist/src → src}/rules/require-algorithm-whitelist/index.js +0 -9
- package/{dist/src → src}/rules/require-audience-validation/index.js +0 -7
- package/{dist/src → src}/rules/require-expiration/index.js +0 -16
- package/{dist/src → src}/rules/require-issued-at/index.js +0 -21
- package/{dist/src → src}/rules/require-issuer-validation/index.js +0 -7
- package/{dist/src → src}/rules/require-max-age/index.js +0 -7
- package/src/types/index.js +2 -0
- package/{dist/src → src}/utils/index.js +1 -74
- package/AGENTS.md +0 -102
- package/CHANGELOG.md +0 -161
- package/dist/AGENTS.md +0 -102
- package/dist/CHANGELOG.md +0 -161
- package/dist/LICENSE +0 -23
- package/dist/README.md +0 -158
- package/dist/package.json +0 -125
- package/dist/src/index.d.ts +0 -101
- package/dist/src/oxlint.d.ts +0 -20
- package/dist/src/oxlint.js +0 -21
- package/dist/src/rules/no-algorithm-confusion/index.d.ts +0 -24
- package/dist/src/rules/no-algorithm-none/index.d.ts +0 -24
- package/dist/src/rules/no-decode-without-verify/index.d.ts +0 -23
- package/dist/src/rules/no-hardcoded-secret/index.d.ts +0 -23
- package/dist/src/rules/no-sensitive-payload/index.d.ts +0 -20
- package/dist/src/rules/no-timestamp-manipulation/index.d.ts +0 -23
- package/dist/src/rules/no-weak-secret/index.d.ts +0 -23
- package/dist/src/rules/require-algorithm-whitelist/index.d.ts +0 -24
- package/dist/src/rules/require-audience-validation/index.d.ts +0 -20
- package/dist/src/rules/require-expiration/index.d.ts +0 -23
- package/dist/src/rules/require-issued-at/index.d.ts +0 -23
- package/dist/src/rules/require-issuer-validation/index.d.ts +0 -20
- package/dist/src/rules/require-max-age/index.d.ts +0 -24
- package/dist/src/types/index.js +0 -7
- package/dist/src/utils/index.d.ts +0 -98
- package/src/__compatibility__/jwt-interface.spec.ts +0 -192
- package/src/index.test.ts +0 -47
- package/src/index.ts +0 -209
- package/src/oxlint.test.ts +0 -61
- package/src/oxlint.ts +0 -23
- package/src/rules/no-algorithm-confusion/index.ts +0 -208
- package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +0 -106
- package/src/rules/no-algorithm-none/index.ts +0 -270
- package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +0 -146
- package/src/rules/no-decode-without-verify/index.ts +0 -162
- package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +0 -76
- package/src/rules/no-hardcoded-secret/index.ts +0 -230
- package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +0 -97
- package/src/rules/no-sensitive-payload/index.ts +0 -123
- package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +0 -157
- package/src/rules/no-timestamp-manipulation/index.ts +0 -118
- package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +0 -54
- package/src/rules/no-weak-secret/index.ts +0 -204
- package/src/rules/no-weak-secret/no-weak-secret.test.ts +0 -114
- package/src/rules/require-algorithm-whitelist/index.ts +0 -145
- package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +0 -72
- package/src/rules/require-audience-validation/index.ts +0 -120
- package/src/rules/require-audience-validation/require-audience-validation.test.ts +0 -68
- package/src/rules/require-expiration/index.ts +0 -170
- package/src/rules/require-expiration/require-expiration.test.ts +0 -135
- package/src/rules/require-issued-at/index.ts +0 -143
- package/src/rules/require-issued-at/require-issued-at.test.ts +0 -64
- package/src/rules/require-issuer-validation/index.ts +0 -120
- package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +0 -68
- package/src/rules/require-max-age/index.ts +0 -117
- package/src/rules/require-max-age/require-max-age.test.ts +0 -69
- package/src/tests/coverage-dual-layer.test.ts +0 -323
- package/src/types/index.ts +0 -107
- package/src/utils/index.ts +0 -285
- package/src/utils/utils.test.ts +0 -324
- /package/{dist/src → src}/types/index.d.ts +0 -0
|
@@ -1,9 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.noTimestampManipulation = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -80,7 +75,6 @@ exports.noTimestampManipulation = (0, eslint_devkit_1.createRule)({
|
|
|
80
75
|
if (!optionsArg) {
|
|
81
76
|
return;
|
|
82
77
|
}
|
|
83
|
-
// Check for noTimestamp: true
|
|
84
78
|
if ((0, utils_1.hasOption)(optionsArg, 'noTimestamp')) {
|
|
85
79
|
const value = (0, utils_1.getOptionValue)(optionsArg, 'noTimestamp');
|
|
86
80
|
if (value?.type === 'Literal' && value.value === true) {
|
|
@@ -1,14 +1,8 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.noWeakSecret = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
5
|
const utils_1 = require("../../utils");
|
|
11
|
-
// Common weak secret patterns
|
|
12
6
|
const WEAK_SECRET_PATTERNS = [
|
|
13
7
|
/^secret$/i,
|
|
14
8
|
/^password$/i,
|
|
@@ -99,25 +93,16 @@ exports.noWeakSecret = (0, eslint_devkit_1.createRule)({
|
|
|
99
93
|
create(context) {
|
|
100
94
|
const options = context.options[0] ?? {};
|
|
101
95
|
const { minSecretLength = 32 } = options;
|
|
102
|
-
/**
|
|
103
|
-
* Check if a value matches known weak patterns
|
|
104
|
-
*/
|
|
105
96
|
const isKnownWeakPattern = (value) => {
|
|
106
97
|
return WEAK_SECRET_PATTERNS.some((pattern) => pattern.test(value));
|
|
107
98
|
};
|
|
108
|
-
/**
|
|
109
|
-
* Check the secret argument for weakness
|
|
110
|
-
*/
|
|
111
99
|
const checkSecret = (secretNode) => {
|
|
112
|
-
// Environment variables are considered safe (configuration)
|
|
113
100
|
if ((0, utils_1.isEnvVariable)(secretNode)) {
|
|
114
101
|
return;
|
|
115
102
|
}
|
|
116
|
-
// Check string literals
|
|
117
103
|
if (secretNode.type === 'Literal' &&
|
|
118
104
|
typeof secretNode.value === 'string') {
|
|
119
105
|
const secretValue = secretNode.value;
|
|
120
|
-
// Check for known weak patterns
|
|
121
106
|
if (isKnownWeakPattern(secretValue)) {
|
|
122
107
|
context.report({
|
|
123
108
|
node: secretNode,
|
|
@@ -125,7 +110,6 @@ exports.noWeakSecret = (0, eslint_devkit_1.createRule)({
|
|
|
125
110
|
});
|
|
126
111
|
return;
|
|
127
112
|
}
|
|
128
|
-
// Check for short secrets
|
|
129
113
|
if (secretValue.length < minSecretLength) {
|
|
130
114
|
context.report({
|
|
131
115
|
node: secretNode,
|
|
@@ -137,7 +121,6 @@ exports.noWeakSecret = (0, eslint_devkit_1.createRule)({
|
|
|
137
121
|
});
|
|
138
122
|
}
|
|
139
123
|
}
|
|
140
|
-
// Template literals with obvious weak values
|
|
141
124
|
if (secretNode.type === 'TemplateLiteral' && secretNode.quasis.length === 1) {
|
|
142
125
|
const rawValue = secretNode.quasis[0].value.raw;
|
|
143
126
|
if (isKnownWeakPattern(rawValue) || rawValue.length < minSecretLength) {
|
|
@@ -150,11 +133,9 @@ exports.noWeakSecret = (0, eslint_devkit_1.createRule)({
|
|
|
150
133
|
};
|
|
151
134
|
return {
|
|
152
135
|
CallExpression(node) {
|
|
153
|
-
// Check both sign and verify operations
|
|
154
136
|
if (!(0, utils_1.isSignOperation)(node) && !(0, utils_1.isVerifyOperation)(node)) {
|
|
155
137
|
return;
|
|
156
138
|
}
|
|
157
|
-
// Secret is usually the second argument
|
|
158
139
|
if (node.arguments.length >= 2) {
|
|
159
140
|
checkSecret(node.arguments[1]);
|
|
160
141
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireAlgorithmWhitelist = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -79,16 +74,13 @@ exports.requireAlgorithmWhitelist = (0, eslint_devkit_1.createRule)({
|
|
|
79
74
|
create(context) {
|
|
80
75
|
return {
|
|
81
76
|
CallExpression(node) {
|
|
82
|
-
// Only check verify operations
|
|
83
77
|
if (!(0, utils_1.isVerifyOperation)(node)) {
|
|
84
78
|
return;
|
|
85
79
|
}
|
|
86
|
-
// Need at least token and key arguments
|
|
87
80
|
if (node.arguments.length < 2) {
|
|
88
81
|
return;
|
|
89
82
|
}
|
|
90
83
|
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
91
|
-
// No options at all - definitely missing algorithms
|
|
92
84
|
if (!optionsArg) {
|
|
93
85
|
context.report({
|
|
94
86
|
node,
|
|
@@ -96,7 +88,6 @@ exports.requireAlgorithmWhitelist = (0, eslint_devkit_1.createRule)({
|
|
|
96
88
|
});
|
|
97
89
|
return;
|
|
98
90
|
}
|
|
99
|
-
// Options exist but no algorithms property
|
|
100
91
|
const hasAlgorithms = (0, utils_1.hasOption)(optionsArg, 'algorithms') ||
|
|
101
92
|
(0, utils_1.hasOption)(optionsArg, 'algorithm') ||
|
|
102
93
|
(0, utils_1.hasOption)(optionsArg, 'alg');
|
|
@@ -1,9 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireAudienceValidation = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -78,7 +73,6 @@ exports.requireAudienceValidation = (0, eslint_devkit_1.createRule)({
|
|
|
78
73
|
return;
|
|
79
74
|
}
|
|
80
75
|
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
81
|
-
// No options at all
|
|
82
76
|
if (!optionsArg) {
|
|
83
77
|
context.report({
|
|
84
78
|
node,
|
|
@@ -86,7 +80,6 @@ exports.requireAudienceValidation = (0, eslint_devkit_1.createRule)({
|
|
|
86
80
|
});
|
|
87
81
|
return;
|
|
88
82
|
}
|
|
89
|
-
// Check for audience option
|
|
90
83
|
if (!(0, utils_1.hasOption)(optionsArg, 'audience') && !(0, utils_1.hasOption)(optionsArg, 'aud')) {
|
|
91
84
|
context.report({
|
|
92
85
|
node: optionsArg,
|
|
@@ -1,9 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireExpiration = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -75,10 +70,6 @@ exports.requireExpiration = (0, eslint_devkit_1.createRule)({
|
|
|
75
70
|
},
|
|
76
71
|
],
|
|
77
72
|
create(context) {
|
|
78
|
-
/**
|
|
79
|
-
* Check if payload contains exp claim
|
|
80
|
-
*/
|
|
81
|
-
// oxlint-disable-next-line consistent-function-scoping
|
|
82
73
|
const payloadHasExp = (payloadNode) => {
|
|
83
74
|
if (payloadNode.type !== 'ObjectExpression') {
|
|
84
75
|
return false;
|
|
@@ -89,25 +80,20 @@ exports.requireExpiration = (0, eslint_devkit_1.createRule)({
|
|
|
89
80
|
};
|
|
90
81
|
return {
|
|
91
82
|
CallExpression(node) {
|
|
92
|
-
// Only check sign operations
|
|
93
83
|
if (!(0, utils_1.isSignOperation)(node)) {
|
|
94
84
|
return;
|
|
95
85
|
}
|
|
96
|
-
// Need at least payload argument
|
|
97
86
|
if (node.arguments.length < 1) {
|
|
98
87
|
return;
|
|
99
88
|
}
|
|
100
89
|
const payloadArg = node.arguments[0];
|
|
101
90
|
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
102
|
-
// Check if payload has exp claim
|
|
103
91
|
if (payloadHasExp(payloadArg)) {
|
|
104
92
|
return;
|
|
105
93
|
}
|
|
106
|
-
// Check if options has expiresIn
|
|
107
94
|
if (optionsArg && (0, utils_1.hasOption)(optionsArg, 'expiresIn')) {
|
|
108
95
|
return;
|
|
109
96
|
}
|
|
110
|
-
// Report missing expiration — suggest adding expiresIn to the options object
|
|
111
97
|
const optionsArg3 = node.arguments[2];
|
|
112
98
|
context.report({
|
|
113
99
|
node,
|
|
@@ -118,11 +104,9 @@ exports.requireExpiration = (0, eslint_devkit_1.createRule)({
|
|
|
118
104
|
fix(fixer) {
|
|
119
105
|
const sourceCode = context.sourceCode;
|
|
120
106
|
if (optionsArg3 && optionsArg3.type === eslint_devkit_1.AST_NODE_TYPES.ObjectExpression) {
|
|
121
|
-
// Options object exists but lacks expiresIn — insert property
|
|
122
107
|
const openBrace = sourceCode.getFirstToken(optionsArg3);
|
|
123
108
|
return fixer.insertTextAfter(openBrace, ' expiresIn: \'1h\',');
|
|
124
109
|
}
|
|
125
|
-
// No options arg — insert { expiresIn: '1h' } as third argument
|
|
126
110
|
const lastArg = node.arguments[node.arguments.length - 1];
|
|
127
111
|
return fixer.insertTextAfter(lastArg, ', { expiresIn: \'1h\' }');
|
|
128
112
|
},
|
|
@@ -1,9 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireIssuedAt = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -62,10 +57,6 @@ exports.requireIssuedAt = (0, eslint_devkit_1.createRule)({
|
|
|
62
57
|
},
|
|
63
58
|
],
|
|
64
59
|
create(context) {
|
|
65
|
-
/**
|
|
66
|
-
* Check if payload contains iat claim
|
|
67
|
-
*/
|
|
68
|
-
// oxlint-disable-next-line consistent-function-scoping
|
|
69
60
|
const payloadHasIat = (payloadNode) => {
|
|
70
61
|
if (payloadNode.type !== 'ObjectExpression') {
|
|
71
62
|
return false;
|
|
@@ -76,36 +67,24 @@ exports.requireIssuedAt = (0, eslint_devkit_1.createRule)({
|
|
|
76
67
|
};
|
|
77
68
|
return {
|
|
78
69
|
CallExpression(node) {
|
|
79
|
-
// Only check sign operations
|
|
80
70
|
if (!(0, utils_1.isSignOperation)(node)) {
|
|
81
71
|
return;
|
|
82
72
|
}
|
|
83
|
-
// Need at least payload argument
|
|
84
73
|
if (node.arguments.length < 1) {
|
|
85
74
|
return;
|
|
86
75
|
}
|
|
87
76
|
const payloadArg = node.arguments[0];
|
|
88
77
|
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
89
|
-
// Check if payload has iat claim
|
|
90
78
|
if (payloadHasIat(payloadArg)) {
|
|
91
79
|
return;
|
|
92
80
|
}
|
|
93
|
-
// Check if options has noTimestamp: false (meaning iat IS added)
|
|
94
|
-
// jsonwebtoken adds iat by default unless noTimestamp: true
|
|
95
81
|
if (optionsArg && (0, utils_1.hasOption)(optionsArg, 'noTimestamp')) {
|
|
96
|
-
// If noTimestamp is explicitly set, we flag it (they're disabling iat)
|
|
97
82
|
context.report({
|
|
98
83
|
node: optionsArg,
|
|
99
84
|
messageId: 'missingIssuedAt',
|
|
100
85
|
});
|
|
101
86
|
return;
|
|
102
87
|
}
|
|
103
|
-
// For explicit payload objects without iat we intentionally do not
|
|
104
|
-
// report — jsonwebtoken adds iat by default. (A dead empty `if` that
|
|
105
|
-
// re-checked !payloadHasIat(payloadArg) — always true after the early
|
|
106
|
-
// return above — was removed as unreachable-by-construction.)
|
|
107
|
-
// Note: jsonwebtoken adds iat by default, so we don't flag by default
|
|
108
|
-
// This rule primarily catches explicit noTimestamp: true usage
|
|
109
88
|
},
|
|
110
89
|
};
|
|
111
90
|
},
|
|
@@ -1,9 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireIssuerValidation = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -78,7 +73,6 @@ exports.requireIssuerValidation = (0, eslint_devkit_1.createRule)({
|
|
|
78
73
|
return;
|
|
79
74
|
}
|
|
80
75
|
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
81
|
-
// No options at all
|
|
82
76
|
if (!optionsArg) {
|
|
83
77
|
context.report({
|
|
84
78
|
node,
|
|
@@ -86,7 +80,6 @@ exports.requireIssuerValidation = (0, eslint_devkit_1.createRule)({
|
|
|
86
80
|
});
|
|
87
81
|
return;
|
|
88
82
|
}
|
|
89
|
-
// Check for issuer option
|
|
90
83
|
if (!(0, utils_1.hasOption)(optionsArg, 'issuer') && !(0, utils_1.hasOption)(optionsArg, 'iss')) {
|
|
91
84
|
context.report({
|
|
92
85
|
node: optionsArg,
|
|
@@ -1,9 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.requireMaxAge = void 0;
|
|
9
4
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
@@ -71,7 +66,6 @@ exports.requireMaxAge = (0, eslint_devkit_1.createRule)({
|
|
|
71
66
|
return;
|
|
72
67
|
}
|
|
73
68
|
const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
|
|
74
|
-
// No options at all
|
|
75
69
|
if (!optionsArg) {
|
|
76
70
|
context.report({
|
|
77
71
|
node,
|
|
@@ -79,7 +73,6 @@ exports.requireMaxAge = (0, eslint_devkit_1.createRule)({
|
|
|
79
73
|
});
|
|
80
74
|
return;
|
|
81
75
|
}
|
|
82
|
-
// Check for maxAge option
|
|
83
76
|
if (!(0, utils_1.hasOption)(optionsArg, 'maxAge') && !(0, utils_1.hasOption)(optionsArg, 'clockTolerance')) {
|
|
84
77
|
context.report({
|
|
85
78
|
node: optionsArg,
|
|
@@ -1,9 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.JWT_METHODS = exports.SENSITIVE_PAYLOAD_FIELDS = exports.SECURE_ALGORITHMS = exports.SYMMETRIC_ALGORITHMS = exports.INSECURE_ALGORITHMS = exports.JWT_LIBRARIES = void 0;
|
|
9
4
|
exports.isJwtLibraryCall = isJwtLibraryCall;
|
|
@@ -18,9 +13,6 @@ exports.isVerifyOperation = isVerifyOperation;
|
|
|
18
13
|
exports.isDecodeOperation = isDecodeOperation;
|
|
19
14
|
exports.getOptionsArgument = getOptionsArgument;
|
|
20
15
|
exports.isTestFile = isTestFile;
|
|
21
|
-
/**
|
|
22
|
-
* Supported JWT libraries
|
|
23
|
-
*/
|
|
24
16
|
exports.JWT_LIBRARIES = {
|
|
25
17
|
JSONWEBTOKEN: 'jsonwebtoken',
|
|
26
18
|
JOSE: 'jose',
|
|
@@ -29,24 +21,15 @@ exports.JWT_LIBRARIES = {
|
|
|
29
21
|
JWKS_RSA: 'jwks-rsa',
|
|
30
22
|
JWT_DECODE: 'jwt-decode',
|
|
31
23
|
};
|
|
32
|
-
/**
|
|
33
|
-
* Insecure algorithms that should be flagged
|
|
34
|
-
*/
|
|
35
24
|
exports.INSECURE_ALGORITHMS = new Set([
|
|
36
25
|
'none',
|
|
37
26
|
'None',
|
|
38
27
|
'NONE',
|
|
39
|
-
'HS256',
|
|
28
|
+
'HS256',
|
|
40
29
|
'HS384',
|
|
41
30
|
'HS512',
|
|
42
31
|
]);
|
|
43
|
-
/**
|
|
44
|
-
* Algorithms vulnerable to confusion attacks when used with asymmetric keys
|
|
45
|
-
*/
|
|
46
32
|
exports.SYMMETRIC_ALGORITHMS = new Set(['HS256', 'HS384', 'HS512']);
|
|
47
|
-
/**
|
|
48
|
-
* Recommended secure algorithms
|
|
49
|
-
*/
|
|
50
33
|
exports.SECURE_ALGORITHMS = new Set([
|
|
51
34
|
'RS256',
|
|
52
35
|
'RS384',
|
|
@@ -59,30 +42,20 @@ exports.SECURE_ALGORITHMS = new Set([
|
|
|
59
42
|
'PS512',
|
|
60
43
|
'EdDSA',
|
|
61
44
|
]);
|
|
62
|
-
/**
|
|
63
|
-
* Sensitive field names that should not be in JWT payload
|
|
64
|
-
* All lowercase for case-insensitive matching
|
|
65
|
-
*/
|
|
66
45
|
exports.SENSITIVE_PAYLOAD_FIELDS = new Set([
|
|
67
|
-
// Passwords
|
|
68
46
|
'password', 'passwd', 'pwd', 'pass', 'secret',
|
|
69
|
-
// API Keys
|
|
70
47
|
'apikey', 'api_key', 'api-key',
|
|
71
48
|
'apisecret', 'api_secret', 'api-secret',
|
|
72
|
-
// Tokens
|
|
73
49
|
'accesstoken', 'access_token', 'access-token',
|
|
74
50
|
'refreshtoken', 'refresh_token', 'refresh-token',
|
|
75
51
|
'bearertoken', 'bearer_token', 'bearer-token',
|
|
76
|
-
// Keys
|
|
77
52
|
'privatekey', 'private_key', 'private-key',
|
|
78
53
|
'secretkey', 'secret_key', 'secret-key',
|
|
79
|
-
// PII - Personal Identifiable Information
|
|
80
54
|
'email', 'emailaddress', 'email_address', 'email-address',
|
|
81
55
|
'phone', 'phonenumber', 'phone_number', 'phone-number',
|
|
82
56
|
'ssn', 'socialsecuritynumber', 'social_security_number',
|
|
83
57
|
'dob', 'dateofbirth', 'date_of_birth', 'birthdate',
|
|
84
58
|
'address', 'streetaddress', 'street_address',
|
|
85
|
-
// Financial
|
|
86
59
|
'creditcard', 'credit_card', 'credit-card',
|
|
87
60
|
'cardnumber', 'card_number', 'card-number',
|
|
88
61
|
'cvv', 'cvc', 'securitycode', 'security_code',
|
|
@@ -91,43 +64,29 @@ exports.SENSITIVE_PAYLOAD_FIELDS = new Set([
|
|
|
91
64
|
'accountnumber', 'account_number', 'account-number',
|
|
92
65
|
'routingnumber', 'routing_number', 'routing-number',
|
|
93
66
|
]);
|
|
94
|
-
/**
|
|
95
|
-
* JWT method patterns for different operations
|
|
96
|
-
*/
|
|
97
67
|
exports.JWT_METHODS = {
|
|
98
68
|
SIGN: new Set(['sign', 'signJWT', 'SignJWT']),
|
|
99
69
|
VERIFY: new Set(['verify', 'verifyJWT', 'jwtVerify']),
|
|
100
70
|
DECODE: new Set(['decode', 'jwtDecode', 'decodeJWT']),
|
|
101
71
|
};
|
|
102
|
-
/**
|
|
103
|
-
* Check if a node represents a call to a JWT library method
|
|
104
|
-
*/
|
|
105
72
|
function isJwtLibraryCall(node, targetMethods) {
|
|
106
|
-
// Check member expression: jwt.verify(), jose.jwtVerify()
|
|
107
73
|
if (node.callee.type === 'MemberExpression') {
|
|
108
74
|
const property = node.callee.property;
|
|
109
75
|
if (property.type === 'Identifier') {
|
|
110
76
|
return targetMethods.has(property.name);
|
|
111
77
|
}
|
|
112
78
|
}
|
|
113
|
-
// Check direct calls: jwtVerify(), jwtDecode()
|
|
114
79
|
if (node.callee.type === 'Identifier') {
|
|
115
80
|
return targetMethods.has(node.callee.name);
|
|
116
81
|
}
|
|
117
82
|
return false;
|
|
118
83
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Check if a node is a string literal with a specific value
|
|
121
|
-
*/
|
|
122
84
|
function isStringLiteral(node, value) {
|
|
123
85
|
if (node.type !== 'Literal' || typeof node.value !== 'string') {
|
|
124
86
|
return false;
|
|
125
87
|
}
|
|
126
88
|
return value === undefined || node.value === value;
|
|
127
89
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Extract algorithm from options object
|
|
130
|
-
*/
|
|
131
90
|
function extractAlgorithms(optionsNode) {
|
|
132
91
|
const algorithms = [];
|
|
133
92
|
for (const prop of optionsNode.properties) {
|
|
@@ -138,11 +97,9 @@ function extractAlgorithms(optionsNode) {
|
|
|
138
97
|
if (keyName !== 'algorithms' && keyName !== 'algorithm' && keyName !== 'alg') {
|
|
139
98
|
continue;
|
|
140
99
|
}
|
|
141
|
-
// Single algorithm: { algorithm: 'HS256' }
|
|
142
100
|
if (prop.value.type === 'Literal' && typeof prop.value.value === 'string') {
|
|
143
101
|
algorithms.push(prop.value.value);
|
|
144
102
|
}
|
|
145
|
-
// Array of algorithms: { algorithms: ['RS256', 'ES256'] }
|
|
146
103
|
if (prop.value.type === 'ArrayExpression') {
|
|
147
104
|
for (const elem of prop.value.elements) {
|
|
148
105
|
if (elem && elem.type === 'Literal' && typeof elem.value === 'string') {
|
|
@@ -153,17 +110,11 @@ function extractAlgorithms(optionsNode) {
|
|
|
153
110
|
}
|
|
154
111
|
return algorithms;
|
|
155
112
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Check if options object has a specific property set
|
|
158
|
-
*/
|
|
159
113
|
function hasOption(optionsNode, optionName) {
|
|
160
114
|
return optionsNode.properties.some((prop) => prop.type === 'Property' &&
|
|
161
115
|
prop.key.type === 'Identifier' &&
|
|
162
116
|
prop.key.name === optionName);
|
|
163
117
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Get the value of a specific option from options object
|
|
166
|
-
*/
|
|
167
118
|
function getOptionValue(optionsNode, optionName) {
|
|
168
119
|
for (const prop of optionsNode.properties) {
|
|
169
120
|
if (prop.type === 'Property' &&
|
|
@@ -174,20 +125,13 @@ function getOptionValue(optionsNode, optionName) {
|
|
|
174
125
|
}
|
|
175
126
|
return undefined;
|
|
176
127
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Check if a literal appears to be a weak secret (short string)
|
|
179
|
-
*/
|
|
180
128
|
function isWeakSecret(node, minLength = 32) {
|
|
181
129
|
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
182
130
|
return node.value.length < minLength;
|
|
183
131
|
}
|
|
184
132
|
return false;
|
|
185
133
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Check if a node is an environment variable access (safe pattern)
|
|
188
|
-
*/
|
|
189
134
|
function isEnvVariable(node) {
|
|
190
|
-
// process.env.JWT_SECRET
|
|
191
135
|
if (node.type === 'MemberExpression' &&
|
|
192
136
|
node.object.type === 'MemberExpression' &&
|
|
193
137
|
node.object.object.type === 'Identifier' &&
|
|
@@ -198,29 +142,15 @@ function isEnvVariable(node) {
|
|
|
198
142
|
}
|
|
199
143
|
return false;
|
|
200
144
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Check if this call looks like a JWT sign operation
|
|
203
|
-
*/
|
|
204
145
|
function isSignOperation(node) {
|
|
205
146
|
return isJwtLibraryCall(node, exports.JWT_METHODS.SIGN);
|
|
206
147
|
}
|
|
207
|
-
/**
|
|
208
|
-
* Check if this call looks like a JWT verify operation
|
|
209
|
-
*/
|
|
210
148
|
function isVerifyOperation(node) {
|
|
211
149
|
return isJwtLibraryCall(node, exports.JWT_METHODS.VERIFY);
|
|
212
150
|
}
|
|
213
|
-
/**
|
|
214
|
-
* Check if this call looks like a JWT decode operation (no verification)
|
|
215
|
-
*/
|
|
216
151
|
function isDecodeOperation(node) {
|
|
217
152
|
return isJwtLibraryCall(node, exports.JWT_METHODS.DECODE);
|
|
218
153
|
}
|
|
219
|
-
/**
|
|
220
|
-
* Get the options argument from a JWT call
|
|
221
|
-
* For jwt.verify(token, secret, options) -> returns options
|
|
222
|
-
* For jwt.sign(payload, secret, options) -> returns options
|
|
223
|
-
*/
|
|
224
154
|
function getOptionsArgument(node, optionsIndex = 2) {
|
|
225
155
|
const arg = node.arguments[optionsIndex];
|
|
226
156
|
if (arg && arg.type === 'ObjectExpression') {
|
|
@@ -228,9 +158,6 @@ function getOptionsArgument(node, optionsIndex = 2) {
|
|
|
228
158
|
}
|
|
229
159
|
return undefined;
|
|
230
160
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Check if file is a test file
|
|
233
|
-
*/
|
|
234
161
|
function isTestFile(filename) {
|
|
235
162
|
return /\.(test|spec)\.[jt]sx?$/.test(filename) ||
|
|
236
163
|
/__(tests?|mocks?)__/.test(filename) ||
|
package/AGENTS.md
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# AGENTS.md
|
|
2
|
-
|
|
3
|
-
> Context for AI coding agents working on eslint-plugin-jwt-security
|
|
4
|
-
|
|
5
|
-
## Setup Commands
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Install dependencies (from monorepo root)
|
|
9
|
-
npm install
|
|
10
|
-
|
|
11
|
-
# Build this package
|
|
12
|
-
nx build eslint-plugin-jwt-security
|
|
13
|
-
|
|
14
|
-
# Run tests
|
|
15
|
-
nx test eslint-plugin-jwt-security
|
|
16
|
-
|
|
17
|
-
# Run tests with coverage
|
|
18
|
-
nx test eslint-plugin-jwt-security --coverage
|
|
19
|
-
|
|
20
|
-
# Lint this package
|
|
21
|
-
nx lint eslint-plugin-jwt-security
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Code Style
|
|
25
|
-
|
|
26
|
-
- TypeScript strict mode with `@interlace/eslint-devkit` types
|
|
27
|
-
- Use `AST_NODE_TYPES` constants, never string literals for node types
|
|
28
|
-
- Use `formatLLMMessage()` for all rule error messages
|
|
29
|
-
- Include CWE, CVSS, OWASP in every security message
|
|
30
|
-
- Use `c8 ignore` comments with documented reasons for untestable code
|
|
31
|
-
- Single-pass AST traversal patterns (O(n) complexity)
|
|
32
|
-
|
|
33
|
-
## Testing Instructions
|
|
34
|
-
|
|
35
|
-
- Tests use `@typescript-eslint/rule-tester` with Vitest
|
|
36
|
-
- Each rule has `index.ts` (implementation) and `*.test.ts` (tests) in same directory
|
|
37
|
-
- Run specific rule test: `nx test eslint-plugin-jwt-security --testPathPattern="no-algorithm-none"`
|
|
38
|
-
- Coverage target: ≥90% lines, ≥95% functions
|
|
39
|
-
- All tests must pass before committing
|
|
40
|
-
|
|
41
|
-
## Project Structure
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
src/
|
|
45
|
-
├── index.ts # Plugin entry, exports rules and configs
|
|
46
|
-
└── rules/ # 13 rule directories
|
|
47
|
-
└── [rule-name]/
|
|
48
|
-
├── index.ts # Rule implementation
|
|
49
|
-
└── *.test.ts # Rule tests
|
|
50
|
-
docs/rules/ # Markdown documentation per rule
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Plugin Purpose
|
|
54
|
-
|
|
55
|
-
Comprehensive JWT security ESLint plugin with **13 rules** covering algorithm attacks, replay prevention, and claim validation. Targets 6 major Node.js JWT libraries.
|
|
56
|
-
|
|
57
|
-
## Rule Categories
|
|
58
|
-
|
|
59
|
-
| Category | Rules | CWEs |
|
|
60
|
-
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ |
|
|
61
|
-
| Algorithm Attacks | `no-algorithm-none`, `no-algorithm-confusion` | 347 |
|
|
62
|
-
| Verification & Secrets | `require-algorithm-whitelist`, `no-decode-without-verify`, `no-weak-secret`, `no-hardcoded-secret`, `no-timestamp-manipulation` | 326, 345, 757, 798 |
|
|
63
|
-
| Claims | `require-expiration`, `require-issued-at`, `require-issuer-validation`, `require-audience-validation`, `require-max-age`, `no-sensitive-payload` | 287, 294, 359, 613 |
|
|
64
|
-
|
|
65
|
-
## Library Detection
|
|
66
|
-
|
|
67
|
-
The plugin detects these method calls:
|
|
68
|
-
|
|
69
|
-
- **Sign**: `jwt.sign()`, `signJWT()`, `SignJWT()`
|
|
70
|
-
- **Verify**: `jwt.verify()`, `jwtVerify()`, `verifyJWT()`
|
|
71
|
-
- **Decode**: `jwt.decode()`, `jwtDecode()`, `decodeJWT()`
|
|
72
|
-
|
|
73
|
-
## Common Fix Patterns
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// Algorithm None Attack (CVE-2022-23540)
|
|
77
|
-
// BAD: jwt.verify(token, secret, { algorithms: ['none'] })
|
|
78
|
-
// GOOD: jwt.verify(token, secret, { algorithms: ['RS256'] })
|
|
79
|
-
|
|
80
|
-
// Algorithm Confusion
|
|
81
|
-
// BAD: jwt.verify(token, publicKey, { algorithms: ['HS256'] })
|
|
82
|
-
// GOOD: jwt.verify(token, publicKey, { algorithms: ['RS256'] })
|
|
83
|
-
|
|
84
|
-
// Hardcoded Secret
|
|
85
|
-
// BAD: jwt.sign(payload, 'my-secret-key')
|
|
86
|
-
// GOOD: jwt.sign(payload, process.env.JWT_SECRET)
|
|
87
|
-
|
|
88
|
-
// Missing Expiration
|
|
89
|
-
// BAD: jwt.sign({ sub: 'user' }, secret)
|
|
90
|
-
// GOOD: jwt.sign({ sub: 'user' }, secret, { expiresIn: '1h' })
|
|
91
|
-
|
|
92
|
-
// Replay Attack Prevention
|
|
93
|
-
// BAD: jwt.sign(payload, secret, { noTimestamp: true })
|
|
94
|
-
// GOOD: jwt.sign(payload, secret)
|
|
95
|
-
// GOOD: jwt.verify(token, secret, { maxAge: '1h' })
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Security Considerations
|
|
99
|
-
|
|
100
|
-
- CWE coverage: 287, 294, 326, 345, 347, 359, 613, 757, 798
|
|
101
|
-
- Detects CVE-2022-23540 (Algorithm None attack)
|
|
102
|
-
- Covers 6 major JWT libraries: jsonwebtoken, jose, jwt-decode, node-jose, jwt-simple, jsonwebtoken-esm
|