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,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for no-algorithm-none rule
|
|
3
|
-
* Security: CWE-347 (CVE-2022-23540)
|
|
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 { noAlgorithmNone } from './index';
|
|
9
|
-
|
|
10
|
-
// Configure RuleTester for Vitest
|
|
11
|
-
RuleTester.afterAll = afterAll;
|
|
12
|
-
RuleTester.it = it;
|
|
13
|
-
RuleTester.itOnly = it.only;
|
|
14
|
-
RuleTester.describe = describe;
|
|
15
|
-
|
|
16
|
-
const ruleTester = new RuleTester({
|
|
17
|
-
languageOptions: {
|
|
18
|
-
parser,
|
|
19
|
-
ecmaVersion: 2022,
|
|
20
|
-
sourceType: 'module',
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('no-algorithm-none', () => {
|
|
25
|
-
describe('Valid Code - Secure Algorithms', () => {
|
|
26
|
-
ruleTester.run('valid - secure algorithms', noAlgorithmNone, {
|
|
27
|
-
valid: [
|
|
28
|
-
// RS256 - RSA with SHA-256
|
|
29
|
-
{ code: `jwt.verify(token, publicKey, { algorithms: ['RS256'] });` },
|
|
30
|
-
// ES256 - ECDSA with SHA-256
|
|
31
|
-
{ code: `jwt.verify(token, publicKey, { algorithms: ['ES256'] });` },
|
|
32
|
-
// Multiple secure algorithms
|
|
33
|
-
{ code: `jwt.verify(token, key, { algorithms: ['RS256', 'RS384', 'RS512'] });` },
|
|
34
|
-
// Single algorithm option
|
|
35
|
-
{ code: `jwt.verify(token, secret, { algorithm: 'HS256' });` },
|
|
36
|
-
// jose library
|
|
37
|
-
{ code: `jwtVerify(token, key, { algorithms: ['RS256'] });` },
|
|
38
|
-
// No options (doesn't trigger this rule)
|
|
39
|
-
{ code: `jwt.verify(token, secret);` },
|
|
40
|
-
// Sign operation with secure algorithm
|
|
41
|
-
{ code: `jwt.sign(payload, secret, { algorithm: 'RS256' });` },
|
|
42
|
-
// Spread operator in options (edge case - line 143 coverage)
|
|
43
|
-
{ code: `jwt.verify(token, key, { ...opts, algorithms: ['RS256'] });` },
|
|
44
|
-
// Non-algorithm properties (line 152 coverage)
|
|
45
|
-
{ code: `jwt.verify(token, key, { issuer: 'auth', complete: true });` },
|
|
46
|
-
// Options without algorithms (different property names)
|
|
47
|
-
{ code: `jwt.sign(payload, secret, { expiresIn: '1h', subject: 'user' });` },
|
|
48
|
-
// Variable algorithms (not literal)
|
|
49
|
-
{ code: `jwt.verify(token, key, { algorithms: allowedAlgorithms });` },
|
|
50
|
-
// Array with non-literal elements
|
|
51
|
-
{ code: `jwt.verify(token, key, { algorithms: [defaultAlg] });` },
|
|
52
|
-
// Non-string literal in array
|
|
53
|
-
{ code: `jwt.verify(token, key, { algorithms: [123] });` },
|
|
54
|
-
// Non-JWT function call - edge case (line 199 coverage)
|
|
55
|
-
{ code: `console.log('hello');` },
|
|
56
|
-
{ code: `someOtherFunction(token, key, { algorithm: 'none' });` },
|
|
57
|
-
],
|
|
58
|
-
invalid: [],
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('Invalid Code - Algorithm None', () => {
|
|
63
|
-
ruleTester.run('invalid - algorithm none', noAlgorithmNone, {
|
|
64
|
-
valid: [],
|
|
65
|
-
invalid: [
|
|
66
|
-
// Direct 'none' algorithm
|
|
67
|
-
{
|
|
68
|
-
code: `jwt.verify(token, secret, { algorithm: 'none' });`,
|
|
69
|
-
errors: [{ messageId: 'algorithmNone' }],
|
|
70
|
-
},
|
|
71
|
-
// 'none' in algorithms array (single element - gets replaced)
|
|
72
|
-
{
|
|
73
|
-
code: `jwt.verify(token, secret, { algorithms: ['none'] });`,
|
|
74
|
-
errors: [{ messageId: 'algorithmNoneInArray' }],
|
|
75
|
-
},
|
|
76
|
-
// 'none' mixed with other algorithms (no auto-fix for removal)
|
|
77
|
-
{
|
|
78
|
-
code: `jwt.verify(token, secret, { algorithms: ['RS256', 'none'] });`,
|
|
79
|
-
errors: [{ messageId: 'algorithmNoneInArray' }],
|
|
80
|
-
},
|
|
81
|
-
// Case insensitive - 'None'
|
|
82
|
-
{
|
|
83
|
-
code: `jwt.verify(token, secret, { algorithm: 'None' });`,
|
|
84
|
-
errors: [{ messageId: 'algorithmNone' }],
|
|
85
|
-
},
|
|
86
|
-
// Case insensitive - 'NONE'
|
|
87
|
-
{
|
|
88
|
-
code: `jwt.verify(token, secret, { algorithms: ['NONE'] });`,
|
|
89
|
-
errors: [{ messageId: 'algorithmNoneInArray' }],
|
|
90
|
-
},
|
|
91
|
-
// alg shorthand
|
|
92
|
-
{
|
|
93
|
-
code: `jwt.verify(token, secret, { alg: 'none' });`,
|
|
94
|
-
errors: [{ messageId: 'algorithmNone' }],
|
|
95
|
-
},
|
|
96
|
-
// Empty algorithms array
|
|
97
|
-
{
|
|
98
|
-
code: `jwt.verify(token, secret, { algorithms: [] });`,
|
|
99
|
-
errors: [{ messageId: 'emptyAlgorithms' }],
|
|
100
|
-
},
|
|
101
|
-
// Sign operation with none
|
|
102
|
-
{
|
|
103
|
-
code: `jwt.sign(payload, secret, { algorithm: 'none' });`,
|
|
104
|
-
errors: [{ messageId: 'algorithmNone' }],
|
|
105
|
-
},
|
|
106
|
-
// jose library
|
|
107
|
-
{
|
|
108
|
-
code: `jwtVerify(token, key, { algorithms: ['none'] });`,
|
|
109
|
-
errors: [{ messageId: 'algorithmNoneInArray' }],
|
|
110
|
-
},
|
|
111
|
-
// signJWT with none
|
|
112
|
-
{
|
|
113
|
-
code: `signJWT(payload, key, { algorithm: 'none' });`,
|
|
114
|
-
errors: [{ messageId: 'algorithmNone' }],
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('Configuration Options', () => {
|
|
121
|
-
ruleTester.run('config - allowInTests', noAlgorithmNone, {
|
|
122
|
-
valid: [
|
|
123
|
-
// Test file with allowInTests enabled
|
|
124
|
-
{
|
|
125
|
-
code: `jwt.verify(token, secret, { algorithm: 'none' });`,
|
|
126
|
-
options: [{ allowInTests: true }],
|
|
127
|
-
filename: 'auth.test.ts',
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
code: `jwt.verify(token, secret, { algorithm: 'none' });`,
|
|
131
|
-
options: [{ allowInTests: true }],
|
|
132
|
-
filename: '__tests__/jwt.ts',
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
invalid: [
|
|
136
|
-
// Non-test file with allowInTests
|
|
137
|
-
{
|
|
138
|
-
code: `jwt.verify(token, secret, { algorithm: 'none' });`,
|
|
139
|
-
options: [{ allowInTests: true }],
|
|
140
|
-
filename: 'auth.ts',
|
|
141
|
-
errors: [{ messageId: 'algorithmNone' }],
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
});
|
|
@@ -1,162 +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: no-decode-without-verify
|
|
9
|
-
*
|
|
10
|
-
* Detects usage of jwt.decode() or jwt-decode library without corresponding
|
|
11
|
-
* verification. Decoded JWTs can be tampered with by attackers.
|
|
12
|
-
*
|
|
13
|
-
* CWE-345: Insufficient Verification of Data Authenticity
|
|
14
|
-
*
|
|
15
|
-
* @see https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JSON_Web_Tokens
|
|
16
|
-
*/
|
|
17
|
-
import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
|
|
18
|
-
import { createRule, formatLLMMessage, MessageIcons, hasSafeAnnotation } from '@interlace/eslint-devkit';
|
|
19
|
-
import { isDecodeOperation } from '../../utils';
|
|
20
|
-
import type { NoDecodeWithoutVerifyOptions } from '../../types';
|
|
21
|
-
|
|
22
|
-
type MessageIds =
|
|
23
|
-
| 'decodeWithoutVerify'
|
|
24
|
-
| 'jwtDecodeLibrary'
|
|
25
|
-
| 'useVerifyInstead';
|
|
26
|
-
|
|
27
|
-
type RuleOptions = [NoDecodeWithoutVerifyOptions?];
|
|
28
|
-
|
|
29
|
-
export const noDecodeWithoutVerify = createRule<RuleOptions, MessageIds>({
|
|
30
|
-
name: 'no-decode-without-verify',
|
|
31
|
-
meta: {
|
|
32
|
-
type: 'problem',
|
|
33
|
-
docs: {
|
|
34
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-decode-without-verify.md',
|
|
35
|
-
description:
|
|
36
|
-
'Disallow trusting decoded JWT payload without signature verification',
|
|
37
|
-
cwe: 'CWE-345',
|
|
38
|
-
cvss: 7.5,
|
|
39
|
-
},
|
|
40
|
-
fixable: undefined,
|
|
41
|
-
hasSuggestions: false,
|
|
42
|
-
messages: {
|
|
43
|
-
decodeWithoutVerify: formatLLMMessage({
|
|
44
|
-
icon: MessageIcons.SECURITY,
|
|
45
|
-
issueName: 'Decoded JWT Without Verification',
|
|
46
|
-
cwe: 'CWE-345',
|
|
47
|
-
description:
|
|
48
|
-
'jwt.decode() returns payload without verifying signature - data can be forged',
|
|
49
|
-
severity: 'HIGH',
|
|
50
|
-
fix: 'Use jwt.verify(token, secret) instead of jwt.decode(token)',
|
|
51
|
-
documentationLink:
|
|
52
|
-
'https://owasp.org/API-Security/0xa7-security-misconfiguration/',
|
|
53
|
-
}),
|
|
54
|
-
jwtDecodeLibrary: formatLLMMessage({
|
|
55
|
-
icon: MessageIcons.SECURITY,
|
|
56
|
-
issueName: 'jwt-decode Library Usage',
|
|
57
|
-
cwe: 'CWE-345',
|
|
58
|
-
description:
|
|
59
|
-
'jwt-decode library only decodes tokens, never verifies signatures',
|
|
60
|
-
severity: 'HIGH',
|
|
61
|
-
fix: 'Use jsonwebtoken.verify() or jose.jwtVerify() for verification',
|
|
62
|
-
documentationLink: 'https://www.npmjs.com/package/jwt-decode',
|
|
63
|
-
}),
|
|
64
|
-
useVerifyInstead: formatLLMMessage({
|
|
65
|
-
icon: MessageIcons.INFO,
|
|
66
|
-
issueName: 'Use Verify Instead',
|
|
67
|
-
description: 'Replace decode with verify to ensure authenticity',
|
|
68
|
-
severity: 'LOW',
|
|
69
|
-
fix: 'jwt.verify(token, secret, { algorithms: ["RS256"] })',
|
|
70
|
-
documentationLink: 'https://www.npmjs.com/package/jsonwebtoken',
|
|
71
|
-
}),
|
|
72
|
-
},
|
|
73
|
-
schema: [
|
|
74
|
-
{
|
|
75
|
-
type: 'object',
|
|
76
|
-
properties: {
|
|
77
|
-
allowHeaderInspection: {
|
|
78
|
-
type: 'boolean',
|
|
79
|
-
default: false,
|
|
80
|
-
description:
|
|
81
|
-
'Allow decode() for reading header before verification',
|
|
82
|
-
},
|
|
83
|
-
trustedSanitizers: {
|
|
84
|
-
type: 'array',
|
|
85
|
-
items: { type: 'string' },
|
|
86
|
-
default: [],
|
|
87
|
-
},
|
|
88
|
-
trustedAnnotations: {
|
|
89
|
-
type: 'array',
|
|
90
|
-
items: { type: 'string' },
|
|
91
|
-
default: ['@decoded-header-only', '@verified-separately'],
|
|
92
|
-
},
|
|
93
|
-
strictMode: {
|
|
94
|
-
type: 'boolean',
|
|
95
|
-
default: false,
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
additionalProperties: false,
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
defaultOptions: [
|
|
103
|
-
{
|
|
104
|
-
allowHeaderInspection: false,
|
|
105
|
-
trustedSanitizers: [],
|
|
106
|
-
trustedAnnotations: ['@decoded-header-only', '@verified-separately'],
|
|
107
|
-
strictMode: false,
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
|
|
111
|
-
const options = context.options[0] ?? {};
|
|
112
|
-
const { trustedAnnotations = [] } = options;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Check if this is a jwt-decode import usage
|
|
116
|
-
*/
|
|
117
|
-
// oxlint-disable-next-line consistent-function-scoping
|
|
118
|
-
const isJwtDecodeLibrary = (node: TSESTree.CallExpression): boolean => {
|
|
119
|
-
// Direct call: jwtDecode(token)
|
|
120
|
-
if (
|
|
121
|
-
node.callee.type === 'Identifier' &&
|
|
122
|
-
(node.callee.name === 'jwtDecode' || node.callee.name === 'jwt_decode')
|
|
123
|
-
) {
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
return false;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
CallExpression(node: TSESTree.CallExpression) {
|
|
131
|
-
// Check for jwt.decode() pattern
|
|
132
|
-
if (isDecodeOperation(node)) {
|
|
133
|
-
// Check for safe annotations
|
|
134
|
-
if (hasSafeAnnotation(node, context, trustedAnnotations)) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
context.report({
|
|
139
|
-
node,
|
|
140
|
-
messageId: 'decodeWithoutVerify',
|
|
141
|
-
});
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Check for jwt-decode library usage
|
|
146
|
-
if (isJwtDecodeLibrary(node)) {
|
|
147
|
-
// Check for safe annotations
|
|
148
|
-
if (hasSafeAnnotation(node, context, trustedAnnotations)) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
context.report({
|
|
153
|
-
node,
|
|
154
|
-
messageId: 'jwtDecodeLibrary',
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
export default noDecodeWithoutVerify;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for no-decode-without-verify rule
|
|
3
|
-
* Security: CWE-345 (Data Authenticity)
|
|
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 { noDecodeWithoutVerify } 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-decode-without-verify', () => {
|
|
24
|
-
describe('Valid Code - Verify Operations', () => {
|
|
25
|
-
ruleTester.run('valid - verify operations', noDecodeWithoutVerify, {
|
|
26
|
-
valid: [
|
|
27
|
-
// jwt.verify is safe
|
|
28
|
-
{ code: `const payload = jwt.verify(token, secret);` },
|
|
29
|
-
{ code: `jwt.verify(token, publicKey, { algorithms: ['RS256'] });` },
|
|
30
|
-
// jose jwtVerify is safe
|
|
31
|
-
{ code: `const { payload } = await jwtVerify(token, key);` },
|
|
32
|
-
// sign is not flagged
|
|
33
|
-
{ code: `jwt.sign(payload, secret);` },
|
|
34
|
-
],
|
|
35
|
-
invalid: [],
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('Invalid Code - Decode Operations', () => {
|
|
40
|
-
ruleTester.run('invalid - decode without verify', noDecodeWithoutVerify, {
|
|
41
|
-
valid: [],
|
|
42
|
-
invalid: [
|
|
43
|
-
// Basic jwt.decode()
|
|
44
|
-
{
|
|
45
|
-
code: `const payload = jwt.decode(token);`,
|
|
46
|
-
errors: [{ messageId: 'decodeWithoutVerify' }],
|
|
47
|
-
},
|
|
48
|
-
// decode with options
|
|
49
|
-
{
|
|
50
|
-
code: `const decoded = jwt.decode(token, { complete: true });`,
|
|
51
|
-
errors: [{ messageId: 'decodeWithoutVerify' }],
|
|
52
|
-
},
|
|
53
|
-
// Using payload directly
|
|
54
|
-
{
|
|
55
|
-
code: `const userId = jwt.decode(token).sub;`,
|
|
56
|
-
errors: [{ messageId: 'decodeWithoutVerify' }],
|
|
57
|
-
},
|
|
58
|
-
// jwt-decode library - jwtDecode pattern
|
|
59
|
-
{
|
|
60
|
-
code: `const payload = jwtDecode(accessToken);`,
|
|
61
|
-
errors: [{ messageId: 'decodeWithoutVerify' }],
|
|
62
|
-
},
|
|
63
|
-
// jwt-decode library - jwt_decode pattern (snake_case import)
|
|
64
|
-
{
|
|
65
|
-
code: `const payload = jwt_decode(accessToken);`,
|
|
66
|
-
errors: [{ messageId: 'jwtDecodeLibrary' }],
|
|
67
|
-
},
|
|
68
|
-
// jose decodeJwt (decode only)
|
|
69
|
-
{
|
|
70
|
-
code: `const payload = decodeJWT(token);`,
|
|
71
|
-
errors: [{ messageId: 'decodeWithoutVerify' }],
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
});
|
|
@@ -1,230 +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: 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;
|