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,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ESLint Rule: no-hardcoded-secret
|
|
9
|
+
*
|
|
10
|
+
* Detects hardcoded secrets in JWT sign/verify operations.
|
|
11
|
+
* Hardcoded secrets in source code are a critical security vulnerability.
|
|
12
|
+
*
|
|
13
|
+
* CWE-798: Use of Hard-coded Credentials
|
|
14
|
+
*
|
|
15
|
+
* @see https://cwe.mitre.org/data/definitions/798.html
|
|
16
|
+
*/
|
|
17
|
+
import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
|
|
18
|
+
import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
|
|
19
|
+
import {
|
|
20
|
+
isSignOperation,
|
|
21
|
+
isVerifyOperation,
|
|
22
|
+
isEnvVariable,
|
|
23
|
+
} from '../../utils';
|
|
24
|
+
import type { NoHardcodedSecretOptions } from '../../types';
|
|
25
|
+
|
|
26
|
+
type MessageIds =
|
|
27
|
+
| 'hardcodedSecret'
|
|
28
|
+
| 'useEnvVariable';
|
|
29
|
+
|
|
30
|
+
type RuleOptions = [NoHardcodedSecretOptions?];
|
|
31
|
+
|
|
32
|
+
export const noHardcodedSecret = createRule<RuleOptions, MessageIds>({
|
|
33
|
+
name: 'no-hardcoded-secret',
|
|
34
|
+
meta: {
|
|
35
|
+
type: 'problem',
|
|
36
|
+
docs: {
|
|
37
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-hardcoded-secret.md',
|
|
38
|
+
description:
|
|
39
|
+
'Disallow hardcoded secrets in JWT sign/verify operations',
|
|
40
|
+
cwe: 'CWE-798',
|
|
41
|
+
cvss: 9.8,
|
|
42
|
+
},
|
|
43
|
+
fixable: undefined,
|
|
44
|
+
hasSuggestions: false,
|
|
45
|
+
messages: {
|
|
46
|
+
hardcodedSecret: formatLLMMessage({
|
|
47
|
+
icon: MessageIcons.SECURITY,
|
|
48
|
+
issueName: 'Hardcoded JWT Secret',
|
|
49
|
+
cwe: 'CWE-798',
|
|
50
|
+
description:
|
|
51
|
+
'Secret key hardcoded in source code can be extracted from repositories',
|
|
52
|
+
severity: 'HIGH',
|
|
53
|
+
fix: 'Use process.env.JWT_SECRET or secure secret management',
|
|
54
|
+
documentationLink: 'https://cwe.mitre.org/data/definitions/798.html',
|
|
55
|
+
}),
|
|
56
|
+
useEnvVariable: formatLLMMessage({
|
|
57
|
+
icon: MessageIcons.INFO,
|
|
58
|
+
issueName: 'Use Environment Variable',
|
|
59
|
+
description: 'Replace hardcoded secret with environment variable',
|
|
60
|
+
severity: 'LOW',
|
|
61
|
+
fix: 'process.env.JWT_SECRET',
|
|
62
|
+
documentationLink:
|
|
63
|
+
'https://12factor.net/config',
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
schema: [
|
|
67
|
+
{
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
envPatterns: {
|
|
71
|
+
type: 'array',
|
|
72
|
+
items: { type: 'string' },
|
|
73
|
+
default: [],
|
|
74
|
+
description: 'Patterns that indicate safe environment variable usage',
|
|
75
|
+
},
|
|
76
|
+
trustedSanitizers: {
|
|
77
|
+
type: 'array',
|
|
78
|
+
items: { type: 'string' },
|
|
79
|
+
default: [],
|
|
80
|
+
},
|
|
81
|
+
trustedAnnotations: {
|
|
82
|
+
type: 'array',
|
|
83
|
+
items: { type: 'string' },
|
|
84
|
+
default: [],
|
|
85
|
+
},
|
|
86
|
+
strictMode: {
|
|
87
|
+
type: 'boolean',
|
|
88
|
+
default: false,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
defaultOptions: [
|
|
96
|
+
{
|
|
97
|
+
envPatterns: [],
|
|
98
|
+
trustedSanitizers: [],
|
|
99
|
+
trustedAnnotations: [],
|
|
100
|
+
strictMode: false,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
|
|
104
|
+
/**
|
|
105
|
+
* Check if a node is a hardcoded string literal
|
|
106
|
+
*/
|
|
107
|
+
// oxlint-disable-next-line consistent-function-scoping
|
|
108
|
+
const isHardcodedString = (node: TSESTree.Node): boolean => {
|
|
109
|
+
// String literal
|
|
110
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Template literal without expressions
|
|
115
|
+
if (
|
|
116
|
+
node.type === 'TemplateLiteral' &&
|
|
117
|
+
node.expressions.length === 0
|
|
118
|
+
) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve an Identifier node to its initializer (one frame of indirection).
|
|
127
|
+
* Closes the audit FN where `const SECRET = 'x'; jwt.sign(p, SECRET)`
|
|
128
|
+
* was treated as safe — the literal-via-const pattern hides the
|
|
129
|
+
* hardcoded secret behind one declaration. See benchmarks/AUDIT_PATTERNS.md
|
|
130
|
+
* §3.2 ("Indirection through one level of variable").
|
|
131
|
+
*/
|
|
132
|
+
const resolveConstLiteralValue = (
|
|
133
|
+
node: TSESTree.Node,
|
|
134
|
+
): TSESTree.Node | null => {
|
|
135
|
+
if (node.type !== 'Identifier') return null;
|
|
136
|
+
const scope = context.sourceCode.getScope?.(node) ?? null;
|
|
137
|
+
if (!scope) return null;
|
|
138
|
+
const variable = scope.references.find(
|
|
139
|
+
(r) => r.identifier === node,
|
|
140
|
+
)?.resolved;
|
|
141
|
+
const def = variable?.defs[0];
|
|
142
|
+
if (!def || def.type !== 'Variable') return null;
|
|
143
|
+
const decl = def.parent;
|
|
144
|
+
if (decl?.type !== 'VariableDeclaration' || decl.kind !== 'const') return null;
|
|
145
|
+
return def.node.init ?? null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if node is a safe key source
|
|
150
|
+
*/
|
|
151
|
+
const isSafeKeySource = (node: TSESTree.Node): boolean => {
|
|
152
|
+
// Environment variable
|
|
153
|
+
if (isEnvVariable(node)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Function call (getSecret(), loadKey(), etc.)
|
|
158
|
+
if (node.type === 'CallExpression') {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// await expression (async key loading)
|
|
163
|
+
if (node.type === 'AwaitExpression') {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Variable reference: resolve `const X = ...` one level. If X is a
|
|
168
|
+
// hardcoded literal, treat as unsafe — fall through to the
|
|
169
|
+
// hardcoded check. Otherwise (env var, call, identifier chain)
|
|
170
|
+
// it's safe.
|
|
171
|
+
if (node.type === 'Identifier') {
|
|
172
|
+
const init = resolveConstLiteralValue(node);
|
|
173
|
+
if (init && isHardcodedString(init)) {
|
|
174
|
+
// Unsafe — let the caller's `isHardcodedString` (with the
|
|
175
|
+
// resolved init) flag it.
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Member expression but not literal (config.secret, etc.)
|
|
182
|
+
if (node.type === 'MemberExpression' && !isHardcodedString(node)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return false;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Like isHardcodedString but resolves single-level `const X = '...'`
|
|
191
|
+
* indirection so the caller fires on `const SECRET = 'x'; jwt.sign(p, SECRET)`.
|
|
192
|
+
*/
|
|
193
|
+
const isHardcodedStringOrResolvedConst = (node: TSESTree.Node): boolean => {
|
|
194
|
+
if (isHardcodedString(node)) return true;
|
|
195
|
+
const init = resolveConstLiteralValue(node);
|
|
196
|
+
return init ? isHardcodedString(init) : false;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
CallExpression(node: TSESTree.CallExpression) {
|
|
201
|
+
// Check both sign and verify operations
|
|
202
|
+
if (!isSignOperation(node) && !isVerifyOperation(node)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Secret/key is the second argument
|
|
207
|
+
if (node.arguments.length < 2) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const secretArg = node.arguments[1];
|
|
212
|
+
|
|
213
|
+
// Skip if safe source
|
|
214
|
+
if (isSafeKeySource(secretArg)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Flag hardcoded strings (also follows single-frame `const X = '...'`).
|
|
219
|
+
if (isHardcodedStringOrResolvedConst(secretArg)) {
|
|
220
|
+
context.report({
|
|
221
|
+
node: secretArg,
|
|
222
|
+
messageId: 'hardcodedSecret',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
export default noHardcodedSecret;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for no-hardcoded-secret rule
|
|
3
|
+
* Security: CWE-798 (Hardcoded Credentials)
|
|
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 { noHardcodedSecret } from './index';
|
|
9
|
+
|
|
10
|
+
RuleTester.afterAll = afterAll;
|
|
11
|
+
RuleTester.it = it;
|
|
12
|
+
RuleTester.itOnly = it.only;
|
|
13
|
+
RuleTester.describe = describe;
|
|
14
|
+
|
|
15
|
+
const ruleTester = new RuleTester({
|
|
16
|
+
languageOptions: {
|
|
17
|
+
parser,
|
|
18
|
+
ecmaVersion: 2022,
|
|
19
|
+
sourceType: 'module',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('no-hardcoded-secret', () => {
|
|
24
|
+
describe('Valid Code - Safe Key Sources', () => {
|
|
25
|
+
ruleTester.run('valid - environment and config', noHardcodedSecret, {
|
|
26
|
+
valid: [
|
|
27
|
+
// Environment variable
|
|
28
|
+
{ code: `jwt.sign(payload, process.env.JWT_SECRET);` },
|
|
29
|
+
{ code: `jwt.verify(token, process.env.JWT_SECRET);` },
|
|
30
|
+
// Variable reference
|
|
31
|
+
{ code: `jwt.sign(payload, secret);` },
|
|
32
|
+
{ code: `jwt.verify(token, privateKey);` },
|
|
33
|
+
// Function call
|
|
34
|
+
{ code: `jwt.sign(payload, getSecretKey());` },
|
|
35
|
+
{ code: `jwt.verify(token, await loadPublicKey());` },
|
|
36
|
+
// Config object
|
|
37
|
+
{ code: `jwt.sign(payload, config.jwtSecret);` },
|
|
38
|
+
{ code: `jwt.verify(token, settings.publicKey);` },
|
|
39
|
+
// Async key loading
|
|
40
|
+
{ code: `jwt.verify(token, await importJWK(jwk));` },
|
|
41
|
+
// crypto operations
|
|
42
|
+
{ code: `jwt.sign(payload, crypto.randomBytes(32));` },
|
|
43
|
+
// Only 1 argument - edge case (line 156 coverage)
|
|
44
|
+
{ code: `jwt.sign(payload);` },
|
|
45
|
+
{ code: `jwt.verify(token);` },
|
|
46
|
+
// MemberExpression that's not hardcoded
|
|
47
|
+
{ code: `jwt.sign(payload, keys.current);` },
|
|
48
|
+
// signJWT with env var
|
|
49
|
+
{ code: `signJWT(payload, process.env.SECRET);` },
|
|
50
|
+
],
|
|
51
|
+
invalid: [],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Invalid Code - Hardcoded Secrets', () => {
|
|
56
|
+
ruleTester.run('invalid - hardcoded strings', noHardcodedSecret, {
|
|
57
|
+
valid: [],
|
|
58
|
+
invalid: [
|
|
59
|
+
// String literal
|
|
60
|
+
{
|
|
61
|
+
code: `jwt.sign(payload, 'my-secret-key');`,
|
|
62
|
+
errors: [{ messageId: 'hardcodedSecret' }],
|
|
63
|
+
},
|
|
64
|
+
// Long string (still hardcoded)
|
|
65
|
+
{
|
|
66
|
+
code: `jwt.sign(payload, 'this-is-a-very-long-secret-but-still-hardcoded');`,
|
|
67
|
+
errors: [{ messageId: 'hardcodedSecret' }],
|
|
68
|
+
},
|
|
69
|
+
// Template literal without variables
|
|
70
|
+
{
|
|
71
|
+
code: 'jwt.sign(payload, `my-secret-key`);',
|
|
72
|
+
errors: [{ messageId: 'hardcodedSecret' }],
|
|
73
|
+
},
|
|
74
|
+
// Verify with hardcoded
|
|
75
|
+
{
|
|
76
|
+
code: `jwt.verify(token, 'hardcoded-public-key');`,
|
|
77
|
+
errors: [{ messageId: 'hardcodedSecret' }],
|
|
78
|
+
},
|
|
79
|
+
// PEM key inline
|
|
80
|
+
{
|
|
81
|
+
code: `jwt.verify(token, '-----BEGIN PUBLIC KEY-----\\\\nMIIBIj...');`,
|
|
82
|
+
errors: [{ messageId: 'hardcodedSecret' }],
|
|
83
|
+
},
|
|
84
|
+
// signJWT with hardcoded
|
|
85
|
+
{
|
|
86
|
+
code: `signJWT(payload, 'hardcoded');`,
|
|
87
|
+
errors: [{ messageId: 'hardcodedSecret' }],
|
|
88
|
+
},
|
|
89
|
+
// sign() direct call with hardcoded
|
|
90
|
+
{
|
|
91
|
+
code: `sign(payload, 'secret123');`,
|
|
92
|
+
errors: [{ messageId: 'hardcodedSecret' }],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ESLint Rule: no-sensitive-payload
|
|
9
|
+
*
|
|
10
|
+
* Detects storage of sensitive data in JWT payload.
|
|
11
|
+
* JWT payloads are only base64-encoded, not encrypted.
|
|
12
|
+
*
|
|
13
|
+
* CWE-359: Exposure of Private Personal Information
|
|
14
|
+
*/
|
|
15
|
+
import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
|
|
16
|
+
import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
|
|
17
|
+
import { isSignOperation, SENSITIVE_PAYLOAD_FIELDS } from '../../utils';
|
|
18
|
+
import type { NoSensitivePayloadOptions } from '../../types';
|
|
19
|
+
|
|
20
|
+
type MessageIds = 'sensitivePayloadField';
|
|
21
|
+
|
|
22
|
+
type RuleOptions = [NoSensitivePayloadOptions?];
|
|
23
|
+
|
|
24
|
+
export const noSensitivePayload = createRule<RuleOptions, MessageIds>({
|
|
25
|
+
name: 'no-sensitive-payload',
|
|
26
|
+
meta: {
|
|
27
|
+
type: 'problem',
|
|
28
|
+
docs: {
|
|
29
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-sensitive-payload.md',
|
|
30
|
+
description:
|
|
31
|
+
'Prevent storing sensitive data in JWT payload which is only base64-encoded',
|
|
32
|
+
cwe: 'CWE-359',
|
|
33
|
+
cvss: 5,
|
|
34
|
+
},
|
|
35
|
+
fixable: undefined,
|
|
36
|
+
hasSuggestions: false,
|
|
37
|
+
messages: {
|
|
38
|
+
sensitivePayloadField: formatLLMMessage({
|
|
39
|
+
icon: MessageIcons.SECURITY,
|
|
40
|
+
issueName: 'Sensitive Data in JWT Payload',
|
|
41
|
+
cwe: 'CWE-359',
|
|
42
|
+
description:
|
|
43
|
+
'JWT payloads are not encrypted - sensitive data like "{{fieldName}}" can be read by anyone',
|
|
44
|
+
severity: 'MEDIUM',
|
|
45
|
+
fix: 'Store sensitive data server-side, reference by ID in token',
|
|
46
|
+
documentationLink: 'https://tools.ietf.org/html/rfc8725',
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
schema: [
|
|
50
|
+
{
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
additionalSensitiveFields: {
|
|
54
|
+
type: 'array',
|
|
55
|
+
items: { type: 'string' },
|
|
56
|
+
default: [],
|
|
57
|
+
description: 'Additional field names to flag as sensitive',
|
|
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
|
+
additionalSensitiveFields: [],
|
|
81
|
+
trustedSanitizers: [],
|
|
82
|
+
trustedAnnotations: [],
|
|
83
|
+
strictMode: false,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
|
|
87
|
+
const options = context.options[0] ?? {};
|
|
88
|
+
const additionalFields = options.additionalSensitiveFields ?? [];
|
|
89
|
+
const allSensitiveFields = new Set([...SENSITIVE_PAYLOAD_FIELDS, ...additionalFields]);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
CallExpression(node: TSESTree.CallExpression) {
|
|
93
|
+
if (!isSignOperation(node)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (node.arguments.length < 1) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const payloadArg = node.arguments[0];
|
|
102
|
+
|
|
103
|
+
// Check object literal payload
|
|
104
|
+
if (payloadArg.type === 'ObjectExpression') {
|
|
105
|
+
for (const prop of payloadArg.properties) {
|
|
106
|
+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
107
|
+
const fieldName = prop.key.name.toLowerCase();
|
|
108
|
+
if (allSensitiveFields.has(fieldName)) {
|
|
109
|
+
context.report({
|
|
110
|
+
node: prop,
|
|
111
|
+
messageId: 'sensitivePayloadField',
|
|
112
|
+
data: { fieldName: prop.key.name },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export default noSensitivePayload;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for no-sensitive-payload 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 { noSensitivePayload } 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('no-sensitive-payload', () => {
|
|
23
|
+
describe('Valid Code - Safe Payloads', () => {
|
|
24
|
+
ruleTester.run('valid - standard claims', noSensitivePayload, {
|
|
25
|
+
valid: [
|
|
26
|
+
// Standard JWT claims
|
|
27
|
+
{ code: `jwt.sign({ sub: 'user123', role: 'admin' }, secret);` },
|
|
28
|
+
{ code: `jwt.sign({ userId: '123', permissions: [] }, secret);` },
|
|
29
|
+
{ code: `jwt.sign({ sub: 'user', iss: 'auth', aud: 'api', exp: 123 }, secret);` },
|
|
30
|
+
// Variable reference (cannot analyze)
|
|
31
|
+
{ code: `jwt.sign(payload, secret);` },
|
|
32
|
+
// Verify operation (not checked)
|
|
33
|
+
{ code: `jwt.verify(token, secret);` },
|
|
34
|
+
// No arguments
|
|
35
|
+
{ code: `jwt.sign();` },
|
|
36
|
+
// Non-JWT sign function
|
|
37
|
+
{ code: `sign(payload, secret);` },
|
|
38
|
+
// Similar but non-sensitive names
|
|
39
|
+
{ code: `jwt.sign({ passwordResetToken: false }, secret);` },
|
|
40
|
+
{ code: `jwt.sign({ emailVerified: true }, secret);` },
|
|
41
|
+
{ code: `jwt.sign({ phoneVerified: true }, secret);` },
|
|
42
|
+
],
|
|
43
|
+
invalid: [],
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('Invalid Code - Password Fields', () => {
|
|
48
|
+
ruleTester.run('invalid - password variations', noSensitivePayload, {
|
|
49
|
+
valid: [],
|
|
50
|
+
invalid: [
|
|
51
|
+
{ code: `jwt.sign({ password: 'secret123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
52
|
+
{ code: `jwt.sign({ passwd: 'abc' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
53
|
+
{ code: `jwt.sign({ pwd: '123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
54
|
+
{ code: `jwt.sign({ pass: 'xyz' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Invalid Code - PII Fields', () => {
|
|
60
|
+
ruleTester.run('invalid - personal identifiable information', noSensitivePayload, {
|
|
61
|
+
valid: [],
|
|
62
|
+
invalid: [
|
|
63
|
+
// Email
|
|
64
|
+
{ code: `jwt.sign({ email: 'user@example.com' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
65
|
+
{ code: `jwt.sign({ emailAddress: 'user@test.com' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
66
|
+
// Phone
|
|
67
|
+
{ code: `jwt.sign({ phone: '555-1234' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
68
|
+
{ code: `jwt.sign({ phoneNumber: '1234567890' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
69
|
+
// SSN
|
|
70
|
+
{ code: `jwt.sign({ ssn: '123-45-6789' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
71
|
+
// Address
|
|
72
|
+
{ code: `jwt.sign({ address: '123 Main St' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
73
|
+
// DOB
|
|
74
|
+
{ code: `jwt.sign({ dob: '1990-01-01' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
75
|
+
{ code: `jwt.sign({ dateOfBirth: '1990-01-01' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('Invalid Code - Financial Fields', () => {
|
|
81
|
+
ruleTester.run('invalid - financial data', noSensitivePayload, {
|
|
82
|
+
valid: [],
|
|
83
|
+
invalid: [
|
|
84
|
+
// Credit card (camelCase)
|
|
85
|
+
{ code: `jwt.sign({ creditCard: '4111111111111111' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
86
|
+
// Credit card (snake_case)
|
|
87
|
+
{ code: `jwt.sign({ credit_card: '4111111111111111' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
88
|
+
// Card number
|
|
89
|
+
{ code: `jwt.sign({ cardNumber: '1234' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
90
|
+
// CVV
|
|
91
|
+
{ code: `jwt.sign({ cvv: '123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
92
|
+
{ code: `jwt.sign({ cvc: '456' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
93
|
+
// PIN
|
|
94
|
+
{ code: `jwt.sign({ pin: '1234' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
95
|
+
// Bank account
|
|
96
|
+
{ code: `jwt.sign({ bankAccount: '123456' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
97
|
+
{ code: `jwt.sign({ accountNumber: '789' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
98
|
+
{ code: `jwt.sign({ routingNumber: '111' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('Invalid Code - API Keys and Secrets', () => {
|
|
104
|
+
ruleTester.run('invalid - secrets and keys', noSensitivePayload, {
|
|
105
|
+
valid: [],
|
|
106
|
+
invalid: [
|
|
107
|
+
// API key variations
|
|
108
|
+
{ code: `jwt.sign({ apiKey: 'sk_live_123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
109
|
+
{ code: `jwt.sign({ api_key: 'sk_live_123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
110
|
+
// Secret
|
|
111
|
+
{ code: `jwt.sign({ secret: 'abc123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
112
|
+
// Token fields
|
|
113
|
+
{ code: `jwt.sign({ accessToken: 'abc123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
114
|
+
{ code: `jwt.sign({ refreshToken: 'xyz789' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
115
|
+
{ code: `jwt.sign({ bearerToken: 'bearer123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
116
|
+
// Private key
|
|
117
|
+
{ code: `jwt.sign({ privateKey: '-----BEGIN RSA' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
118
|
+
{ code: `jwt.sign({ secretKey: 'key123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Invalid Code - Multiple Sensitive Fields', () => {
|
|
124
|
+
ruleTester.run('invalid - multiple violations', noSensitivePayload, {
|
|
125
|
+
valid: [],
|
|
126
|
+
invalid: [
|
|
127
|
+
// Two sensitive fields - should report both
|
|
128
|
+
{
|
|
129
|
+
code: `jwt.sign({ email: 'user@test.com', phone: '555-1234' }, secret);`,
|
|
130
|
+
errors: [
|
|
131
|
+
{ messageId: 'sensitivePayloadField' },
|
|
132
|
+
{ messageId: 'sensitivePayloadField' },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
// Mixed valid and invalid
|
|
136
|
+
{
|
|
137
|
+
code: `jwt.sign({ sub: 'user', password: 'secret' }, secret);`,
|
|
138
|
+
errors: [{ messageId: 'sensitivePayloadField' }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('Edge Cases - Custom Configuration', () => {
|
|
145
|
+
ruleTester.run('custom sensitive fields', noSensitivePayload, {
|
|
146
|
+
valid: [],
|
|
147
|
+
invalid: [
|
|
148
|
+
// Test additionalSensitiveFields option
|
|
149
|
+
{
|
|
150
|
+
code: `jwt.sign({ customSecret: 'value' }, secret);`,
|
|
151
|
+
options: [{ additionalSensitiveFields: ['customsecret'] }],
|
|
152
|
+
errors: [{ messageId: 'sensitivePayloadField' }],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|