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,98 +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
|
-
* Shared utilities for JWT security rules
|
|
8
|
-
*
|
|
9
|
-
* Library detection, pattern matching, and common helpers.
|
|
10
|
-
*/
|
|
11
|
-
import type { TSESTree } from '@interlace/eslint-devkit';
|
|
12
|
-
/**
|
|
13
|
-
* Supported JWT libraries
|
|
14
|
-
*/
|
|
15
|
-
export declare const JWT_LIBRARIES: {
|
|
16
|
-
readonly JSONWEBTOKEN: "jsonwebtoken";
|
|
17
|
-
readonly JOSE: "jose";
|
|
18
|
-
readonly EXPRESS_JWT: "express-jwt";
|
|
19
|
-
readonly NESTJS_JWT: "@nestjs/jwt";
|
|
20
|
-
readonly JWKS_RSA: "jwks-rsa";
|
|
21
|
-
readonly JWT_DECODE: "jwt-decode";
|
|
22
|
-
};
|
|
23
|
-
export type JwtLibrary = (typeof JWT_LIBRARIES)[keyof typeof JWT_LIBRARIES];
|
|
24
|
-
/**
|
|
25
|
-
* Insecure algorithms that should be flagged
|
|
26
|
-
*/
|
|
27
|
-
export declare const INSECURE_ALGORITHMS: Set<string>;
|
|
28
|
-
/**
|
|
29
|
-
* Algorithms vulnerable to confusion attacks when used with asymmetric keys
|
|
30
|
-
*/
|
|
31
|
-
export declare const SYMMETRIC_ALGORITHMS: Set<string>;
|
|
32
|
-
/**
|
|
33
|
-
* Recommended secure algorithms
|
|
34
|
-
*/
|
|
35
|
-
export declare const SECURE_ALGORITHMS: Set<string>;
|
|
36
|
-
/**
|
|
37
|
-
* Sensitive field names that should not be in JWT payload
|
|
38
|
-
* All lowercase for case-insensitive matching
|
|
39
|
-
*/
|
|
40
|
-
export declare const SENSITIVE_PAYLOAD_FIELDS: Set<string>;
|
|
41
|
-
/**
|
|
42
|
-
* JWT method patterns for different operations
|
|
43
|
-
*/
|
|
44
|
-
export declare const JWT_METHODS: {
|
|
45
|
-
readonly SIGN: Set<string>;
|
|
46
|
-
readonly VERIFY: Set<string>;
|
|
47
|
-
readonly DECODE: Set<string>;
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Check if a node represents a call to a JWT library method
|
|
51
|
-
*/
|
|
52
|
-
export declare function isJwtLibraryCall(node: TSESTree.CallExpression, targetMethods: Set<string>): boolean;
|
|
53
|
-
/**
|
|
54
|
-
* Check if a node is a string literal with a specific value
|
|
55
|
-
*/
|
|
56
|
-
export declare function isStringLiteral(node: TSESTree.Node, value?: string): node is TSESTree.Literal;
|
|
57
|
-
/**
|
|
58
|
-
* Extract algorithm from options object
|
|
59
|
-
*/
|
|
60
|
-
export declare function extractAlgorithms(optionsNode: TSESTree.ObjectExpression): string[];
|
|
61
|
-
/**
|
|
62
|
-
* Check if options object has a specific property set
|
|
63
|
-
*/
|
|
64
|
-
export declare function hasOption(optionsNode: TSESTree.ObjectExpression, optionName: string): boolean;
|
|
65
|
-
/**
|
|
66
|
-
* Get the value of a specific option from options object
|
|
67
|
-
*/
|
|
68
|
-
export declare function getOptionValue(optionsNode: TSESTree.ObjectExpression, optionName: string): TSESTree.Node | undefined;
|
|
69
|
-
/**
|
|
70
|
-
* Check if a literal appears to be a weak secret (short string)
|
|
71
|
-
*/
|
|
72
|
-
export declare function isWeakSecret(node: TSESTree.Node, minLength?: number): boolean;
|
|
73
|
-
/**
|
|
74
|
-
* Check if a node is an environment variable access (safe pattern)
|
|
75
|
-
*/
|
|
76
|
-
export declare function isEnvVariable(node: TSESTree.Node): boolean;
|
|
77
|
-
/**
|
|
78
|
-
* Check if this call looks like a JWT sign operation
|
|
79
|
-
*/
|
|
80
|
-
export declare function isSignOperation(node: TSESTree.CallExpression): boolean;
|
|
81
|
-
/**
|
|
82
|
-
* Check if this call looks like a JWT verify operation
|
|
83
|
-
*/
|
|
84
|
-
export declare function isVerifyOperation(node: TSESTree.CallExpression): boolean;
|
|
85
|
-
/**
|
|
86
|
-
* Check if this call looks like a JWT decode operation (no verification)
|
|
87
|
-
*/
|
|
88
|
-
export declare function isDecodeOperation(node: TSESTree.CallExpression): boolean;
|
|
89
|
-
/**
|
|
90
|
-
* Get the options argument from a JWT call
|
|
91
|
-
* For jwt.verify(token, secret, options) -> returns options
|
|
92
|
-
* For jwt.sign(payload, secret, options) -> returns options
|
|
93
|
-
*/
|
|
94
|
-
export declare function getOptionsArgument(node: TSESTree.CallExpression, optionsIndex?: number): TSESTree.ObjectExpression | undefined;
|
|
95
|
-
/**
|
|
96
|
-
* Check if file is a test file
|
|
97
|
-
*/
|
|
98
|
-
export declare function isTestFile(filename: string): boolean;
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JWT Ecosystem Interface Compatibility Tests
|
|
3
|
-
*
|
|
4
|
-
* Verifies that JWT-related packages export the interfaces our ESLint rules depend on.
|
|
5
|
-
*
|
|
6
|
-
* @sdk jsonwebtoken, jose, express-jwt, jwks-rsa, jwt-decode
|
|
7
|
-
* @last-updated 2026-01-02
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, beforeAll } from 'vitest';
|
|
11
|
-
|
|
12
|
-
// Use unknown types to avoid ESM/CJS type conflicts at compile time
|
|
13
|
-
// Runtime checks handle the actual exports
|
|
14
|
-
let jsonwebtokenModule: unknown;
|
|
15
|
-
let joseModule: unknown;
|
|
16
|
-
let jwtDecodeModule: unknown;
|
|
17
|
-
|
|
18
|
-
// Helper to safely access properties on unknown types
|
|
19
|
-
const getProp = (obj: unknown, key: string): unknown => {
|
|
20
|
-
if (obj && typeof obj === 'object' && key in obj) {
|
|
21
|
-
return (obj as Record<string, unknown>)[key];
|
|
22
|
-
}
|
|
23
|
-
return undefined;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
beforeAll(async () => {
|
|
27
|
-
try {
|
|
28
|
-
jsonwebtokenModule = await import('jsonwebtoken');
|
|
29
|
-
} catch {
|
|
30
|
-
console.warn('jsonwebtoken not installed');
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
joseModule = await import('jose');
|
|
34
|
-
} catch {
|
|
35
|
-
console.warn('jose not installed');
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
jwtDecodeModule = await import('jwt-decode');
|
|
39
|
-
} catch {
|
|
40
|
-
console.warn('jwt-decode not installed');
|
|
41
|
-
}
|
|
42
|
-
}, 30_000); // native-addon packages can take >10s to load cold
|
|
43
|
-
|
|
44
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
45
|
-
// JSONWEBTOKEN INTERFACE
|
|
46
|
-
// Primary package for JWT operations in Node.js
|
|
47
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
48
|
-
|
|
49
|
-
describe('jsonwebtoken Interface Compatibility', () => {
|
|
50
|
-
describe('Core Exports', () => {
|
|
51
|
-
it('exports sign function', () => {
|
|
52
|
-
const sign = getProp(jsonwebtokenModule, 'sign');
|
|
53
|
-
expect(sign).toBeDefined();
|
|
54
|
-
expect(typeof sign).toBe('function');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('exports verify function', () => {
|
|
58
|
-
const verify = getProp(jsonwebtokenModule, 'verify');
|
|
59
|
-
expect(verify).toBeDefined();
|
|
60
|
-
expect(typeof verify).toBe('function');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('exports decode function', () => {
|
|
64
|
-
const decode = getProp(jsonwebtokenModule, 'decode');
|
|
65
|
-
expect(decode).toBeDefined();
|
|
66
|
-
expect(typeof decode).toBe('function');
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('Sign Options (Critical for Security Rules)', () => {
|
|
71
|
-
it('sign accepts algorithm option', () => {
|
|
72
|
-
// Rules check: no-algorithm-none, require-algorithm-whitelist
|
|
73
|
-
expect(getProp(jsonwebtokenModule, 'sign')).toBeDefined();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('sign accepts expiresIn option', () => {
|
|
77
|
-
// Rules check: require-expiration
|
|
78
|
-
expect(getProp(jsonwebtokenModule, 'sign')).toBeDefined();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('sign accepts audience option', () => {
|
|
82
|
-
// Rules check: require-audience
|
|
83
|
-
expect(getProp(jsonwebtokenModule, 'sign')).toBeDefined();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('sign accepts issuer option', () => {
|
|
87
|
-
// Rules check: require-issuer
|
|
88
|
-
expect(getProp(jsonwebtokenModule, 'sign')).toBeDefined();
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('Verify Options (Critical for Security Rules)', () => {
|
|
93
|
-
it('verify accepts algorithms option (array)', () => {
|
|
94
|
-
// Rules check: verify-algorithm-explicit
|
|
95
|
-
expect(getProp(jsonwebtokenModule, 'verify')).toBeDefined();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('verify accepts complete option', () => {
|
|
99
|
-
// Returns full decoded token with header
|
|
100
|
-
expect(getProp(jsonwebtokenModule, 'verify')).toBeDefined();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
106
|
-
// JOSE INTERFACE
|
|
107
|
-
// Modern JWT library with async/await support
|
|
108
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
109
|
-
|
|
110
|
-
describe('jose Interface Compatibility', () => {
|
|
111
|
-
describe('Core Exports', () => {
|
|
112
|
-
it('exports SignJWT class', () => {
|
|
113
|
-
expect(getProp(joseModule, 'SignJWT')).toBeDefined();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('exports jwtVerify function', () => {
|
|
117
|
-
const jwtVerify = getProp(joseModule, 'jwtVerify');
|
|
118
|
-
expect(jwtVerify).toBeDefined();
|
|
119
|
-
expect(typeof jwtVerify).toBe('function');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('exports jwtDecrypt function', () => {
|
|
123
|
-
const jwtDecrypt = getProp(joseModule, 'jwtDecrypt');
|
|
124
|
-
expect(jwtDecrypt).toBeDefined();
|
|
125
|
-
expect(typeof jwtDecrypt).toBe('function');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('exports createRemoteJWKSet function', () => {
|
|
129
|
-
// Used for JWKS validation
|
|
130
|
-
const createRemoteJWKSet = getProp(joseModule, 'createRemoteJWKSet');
|
|
131
|
-
expect(createRemoteJWKSet).toBeDefined();
|
|
132
|
-
expect(typeof createRemoteJWKSet).toBe('function');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('Key Generation', () => {
|
|
137
|
-
it('exports generateKeyPair function', () => {
|
|
138
|
-
expect(getProp(joseModule, 'generateKeyPair')).toBeDefined();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('exports generateSecret function', () => {
|
|
142
|
-
expect(getProp(joseModule, 'generateSecret')).toBeDefined();
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
148
|
-
// JWT-DECODE INTERFACE
|
|
149
|
-
// Client-side JWT decoding (no verification)
|
|
150
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
151
|
-
|
|
152
|
-
describe('jwt-decode Interface Compatibility', () => {
|
|
153
|
-
describe('Core Exports', () => {
|
|
154
|
-
it('exports jwtDecode function', () => {
|
|
155
|
-
// Can be either named export or default export depending on version
|
|
156
|
-
const jwtDecode = getProp(jwtDecodeModule, 'jwtDecode') ?? getProp(jwtDecodeModule, 'default');
|
|
157
|
-
expect(jwtDecode).toBeDefined();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('exports InvalidTokenError', () => {
|
|
161
|
-
expect(getProp(jwtDecodeModule, 'InvalidTokenError')).toBeDefined();
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
167
|
-
// PACKAGE METADATA
|
|
168
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
169
|
-
|
|
170
|
-
describe('Package Metadata', () => {
|
|
171
|
-
it('jsonwebtoken has discoverable version', async () => {
|
|
172
|
-
try {
|
|
173
|
-
const pkgPath = require.resolve('jsonwebtoken/package.json');
|
|
174
|
-
const pkg = await import(pkgPath, { with: { type: 'json' } }).then(m => m.default);
|
|
175
|
-
expect(pkg.version).toBeDefined();
|
|
176
|
-
console.log(`📦 jsonwebtoken version: ${pkg.version}`);
|
|
177
|
-
} catch {
|
|
178
|
-
console.log('📦 jsonwebtoken not installed');
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('jose has discoverable version', async () => {
|
|
183
|
-
try {
|
|
184
|
-
const pkgPath = require.resolve('jose/package.json');
|
|
185
|
-
const pkg = await import(pkgPath, { with: { type: 'json' } }).then(m => m.default);
|
|
186
|
-
expect(pkg.version).toBeDefined();
|
|
187
|
-
console.log(`📦 jose version: ${pkg.version}`);
|
|
188
|
-
} catch {
|
|
189
|
-
console.log('📦 jose not installed');
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
});
|
package/src/index.test.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import plugin from './index';
|
|
3
|
-
import { rules, configs } from './index';
|
|
4
|
-
|
|
5
|
-
describe('eslint-plugin-jwt-security plugin interface', () => {
|
|
6
|
-
it('should export correct meta information', () => {
|
|
7
|
-
expect(plugin.meta).toBeDefined();
|
|
8
|
-
expect(plugin.meta?.name).toBe('eslint-plugin-jwt-security');
|
|
9
|
-
expect(plugin.meta?.version).toBeDefined();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should export all JWT security rules', () => {
|
|
13
|
-
expect(plugin.rules).toBeDefined();
|
|
14
|
-
const ruleNames = Object.keys(plugin.rules || {});
|
|
15
|
-
expect(ruleNames).toContain('no-algorithm-none');
|
|
16
|
-
expect(ruleNames).toContain('no-algorithm-confusion');
|
|
17
|
-
expect(ruleNames).toContain('no-timestamp-manipulation');
|
|
18
|
-
expect(ruleNames.length).toBe(13);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('configurations', () => {
|
|
22
|
-
it('should provide recommended configuration', () => {
|
|
23
|
-
expect(configs.recommended).toBeDefined();
|
|
24
|
-
expect(configs.recommended.plugins?.jwt).toBeDefined();
|
|
25
|
-
|
|
26
|
-
const recommendedRules = configs.recommended.rules || {};
|
|
27
|
-
Object.keys(recommendedRules).forEach(ruleName => {
|
|
28
|
-
expect(ruleName).toMatch(/^jwt\//);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
expect(recommendedRules['jwt/no-algorithm-none']).toBe('error');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should provide strict configuration', () => {
|
|
35
|
-
expect(configs.strict).toBeDefined();
|
|
36
|
-
expect(configs.strict.plugins?.jwt).toBeDefined();
|
|
37
|
-
|
|
38
|
-
const strictRules = configs.strict.rules || {};
|
|
39
|
-
Object.keys(strictRules).forEach(ruleName => {
|
|
40
|
-
expect(ruleName).toMatch(/^jwt\//);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
expect(strictRules['jwt/no-algorithm-none']).toBe('error');
|
|
44
|
-
expect(Object.keys(strictRules).length).toBe(Object.keys(rules).length);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,209 +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-plugin-jwt-security
|
|
9
|
-
*
|
|
10
|
-
* Comprehensive JWT security ESLint plugin for Node.js applications.
|
|
11
|
-
* Detects vulnerabilities across jsonwebtoken, jose, express-jwt,
|
|
12
|
-
* @nestjs/jwt, jwks-rsa, and jwt-decode libraries.
|
|
13
|
-
*
|
|
14
|
-
* Features:
|
|
15
|
-
* - Algorithm confusion attack detection (CVE-2022-23540)
|
|
16
|
-
* - "Back to the Future" replay attack prevention (LightSEC 2025)
|
|
17
|
-
* - Signature verification enforcement
|
|
18
|
-
* - Weak secret detection
|
|
19
|
-
* - Expiration and claim validation
|
|
20
|
-
* - AI-assisted fix guidance with CWE references
|
|
21
|
-
*
|
|
22
|
-
* @see https://tools.ietf.org/html/rfc8725
|
|
23
|
-
* @see https://securitypattern.com/post/jwt-back-to-the-future
|
|
24
|
-
*/
|
|
25
|
-
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
26
|
-
|
|
27
|
-
// Import rules - Core Security
|
|
28
|
-
import { noAlgorithmNone } from './rules/no-algorithm-none';
|
|
29
|
-
import { noAlgorithmConfusion } from './rules/no-algorithm-confusion';
|
|
30
|
-
import { requireAlgorithmWhitelist } from './rules/require-algorithm-whitelist';
|
|
31
|
-
import { noDecodeWithoutVerify } from './rules/no-decode-without-verify';
|
|
32
|
-
import { noWeakSecret } from './rules/no-weak-secret';
|
|
33
|
-
import { requireExpiration } from './rules/require-expiration';
|
|
34
|
-
import { noHardcodedSecret } from './rules/no-hardcoded-secret';
|
|
35
|
-
|
|
36
|
-
// Import rules - 2025 Vulnerability Research
|
|
37
|
-
import { requireIssuedAt } from './rules/require-issued-at';
|
|
38
|
-
import { requireIssuerValidation } from './rules/require-issuer-validation';
|
|
39
|
-
import { requireAudienceValidation } from './rules/require-audience-validation';
|
|
40
|
-
import { noTimestampManipulation } from './rules/no-timestamp-manipulation';
|
|
41
|
-
import { requireMaxAge } from './rules/require-max-age';
|
|
42
|
-
import { noSensitivePayload } from './rules/no-sensitive-payload';
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Collection of all JWT security ESLint rules (13 rules)
|
|
46
|
-
*/
|
|
47
|
-
export const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
48
|
-
// Core Security Rules
|
|
49
|
-
'no-algorithm-none': noAlgorithmNone,
|
|
50
|
-
'no-algorithm-confusion': noAlgorithmConfusion,
|
|
51
|
-
'require-algorithm-whitelist': requireAlgorithmWhitelist,
|
|
52
|
-
'no-decode-without-verify': noDecodeWithoutVerify,
|
|
53
|
-
'no-weak-secret': noWeakSecret,
|
|
54
|
-
'require-expiration': requireExpiration,
|
|
55
|
-
'no-hardcoded-secret': noHardcodedSecret,
|
|
56
|
-
|
|
57
|
-
// 2025 Vulnerability Research - "Back to the Future" & Replay Prevention
|
|
58
|
-
'require-issued-at': requireIssuedAt,
|
|
59
|
-
'require-issuer-validation': requireIssuerValidation,
|
|
60
|
-
'require-audience-validation': requireAudienceValidation,
|
|
61
|
-
'no-timestamp-manipulation': noTimestampManipulation,
|
|
62
|
-
'require-max-age': requireMaxAge,
|
|
63
|
-
'no-sensitive-payload': noSensitivePayload,
|
|
64
|
-
} satisfies Record<string, TSESLint.RuleModule<string, readonly unknown[]>>;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* ESLint Plugin object
|
|
68
|
-
*/
|
|
69
|
-
export const plugin: TSESLint.FlatConfig.Plugin = {
|
|
70
|
-
meta: {
|
|
71
|
-
name: 'eslint-plugin-jwt-security',
|
|
72
|
-
version: '2.2.9',
|
|
73
|
-
},
|
|
74
|
-
rules,
|
|
75
|
-
} satisfies TSESLint.FlatConfig.Plugin;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Recommended configuration - baseline security
|
|
79
|
-
* Critical rules as errors, high as warnings
|
|
80
|
-
*/
|
|
81
|
-
const recommendedRules: Record<string, TSESLint.FlatConfig.RuleEntry> = {
|
|
82
|
-
// CRITICAL - Algorithm attacks
|
|
83
|
-
'jwt/no-algorithm-none': 'error',
|
|
84
|
-
'jwt/no-algorithm-confusion': 'error',
|
|
85
|
-
|
|
86
|
-
// HIGH - Verification and secrets
|
|
87
|
-
'jwt/require-algorithm-whitelist': 'error',
|
|
88
|
-
'jwt/no-decode-without-verify': 'error',
|
|
89
|
-
'jwt/no-weak-secret': 'error',
|
|
90
|
-
'jwt/no-hardcoded-secret': 'error',
|
|
91
|
-
'jwt/no-timestamp-manipulation': 'error', // LightSEC 2025
|
|
92
|
-
|
|
93
|
-
// MEDIUM - Best practices
|
|
94
|
-
'jwt/require-expiration': 'error',
|
|
95
|
-
'jwt/no-sensitive-payload': 'warn',
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Strict configuration - maximum security (2025 research)
|
|
100
|
-
* All rules as errors - includes replay attack prevention
|
|
101
|
-
*/
|
|
102
|
-
const strictRules: Record<string, TSESLint.FlatConfig.RuleEntry> = {
|
|
103
|
-
// All core rules as errors
|
|
104
|
-
'jwt/no-algorithm-none': 'error',
|
|
105
|
-
'jwt/no-algorithm-confusion': 'error',
|
|
106
|
-
'jwt/require-algorithm-whitelist': 'error',
|
|
107
|
-
'jwt/no-decode-without-verify': 'error',
|
|
108
|
-
'jwt/no-weak-secret': 'error',
|
|
109
|
-
'jwt/no-hardcoded-secret': 'error',
|
|
110
|
-
'jwt/require-expiration': 'error',
|
|
111
|
-
|
|
112
|
-
// 2025 Research - Full replay attack prevention
|
|
113
|
-
'jwt/require-issued-at': 'error',
|
|
114
|
-
'jwt/require-issuer-validation': 'error',
|
|
115
|
-
'jwt/require-audience-validation': 'error',
|
|
116
|
-
'jwt/no-timestamp-manipulation': 'error',
|
|
117
|
-
'jwt/require-max-age': 'error',
|
|
118
|
-
'jwt/no-sensitive-payload': 'error',
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Legacy configuration - migration mode
|
|
123
|
-
* Only critical rules enabled
|
|
124
|
-
*/
|
|
125
|
-
const legacyRules: Record<string, TSESLint.FlatConfig.RuleEntry> = {
|
|
126
|
-
// Only most critical rules
|
|
127
|
-
'jwt/no-algorithm-none': 'error',
|
|
128
|
-
'jwt/no-algorithm-confusion': 'error',
|
|
129
|
-
'jwt/no-hardcoded-secret': 'warn',
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Configuration presets for flat config
|
|
134
|
-
*/
|
|
135
|
-
export const configs = {
|
|
136
|
-
/**
|
|
137
|
-
* Flagship preset — the rule from this plugin in the ecosystem-wide
|
|
138
|
-
* flagship list (`.agent/flagship-rules.md`). Use this when you want
|
|
139
|
-
* the highest-signal JWT subset shippable in CI gates.
|
|
140
|
-
*/
|
|
141
|
-
flagship: {
|
|
142
|
-
plugins: {
|
|
143
|
-
jwt: plugin,
|
|
144
|
-
},
|
|
145
|
-
rules: {
|
|
146
|
-
'jwt/no-algorithm-none': 'error',
|
|
147
|
-
},
|
|
148
|
-
} satisfies TSESLint.FlatConfig.Config,
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Recommended preset - balanced security
|
|
152
|
-
*/
|
|
153
|
-
recommended: {
|
|
154
|
-
plugins: {
|
|
155
|
-
jwt: plugin,
|
|
156
|
-
},
|
|
157
|
-
rules: recommendedRules,
|
|
158
|
-
} satisfies TSESLint.FlatConfig.Config,
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Strict preset - maximum security (includes 2025 research)
|
|
162
|
-
*/
|
|
163
|
-
strict: {
|
|
164
|
-
plugins: {
|
|
165
|
-
jwt: plugin,
|
|
166
|
-
},
|
|
167
|
-
rules: strictRules,
|
|
168
|
-
} satisfies TSESLint.FlatConfig.Config,
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Legacy preset - migration mode
|
|
172
|
-
*/
|
|
173
|
-
legacy: {
|
|
174
|
-
plugins: {
|
|
175
|
-
jwt: plugin,
|
|
176
|
-
},
|
|
177
|
-
rules: legacyRules,
|
|
178
|
-
} satisfies TSESLint.FlatConfig.Config,
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* All rules preset
|
|
182
|
-
*/
|
|
183
|
-
all: {
|
|
184
|
-
plugins: {
|
|
185
|
-
jwt: plugin,
|
|
186
|
-
},
|
|
187
|
-
rules: strictRules,
|
|
188
|
-
} satisfies TSESLint.FlatConfig.Config,
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// Default export for flat config usage
|
|
192
|
-
export default plugin;
|
|
193
|
-
|
|
194
|
-
// Named exports for flexibility
|
|
195
|
-
export {
|
|
196
|
-
noAlgorithmNone,
|
|
197
|
-
noAlgorithmConfusion,
|
|
198
|
-
requireAlgorithmWhitelist,
|
|
199
|
-
noDecodeWithoutVerify,
|
|
200
|
-
noWeakSecret,
|
|
201
|
-
requireExpiration,
|
|
202
|
-
noHardcodedSecret,
|
|
203
|
-
requireIssuedAt,
|
|
204
|
-
requireIssuerValidation,
|
|
205
|
-
requireAudienceValidation,
|
|
206
|
-
noTimestampManipulation,
|
|
207
|
-
requireMaxAge,
|
|
208
|
-
noSensitivePayload,
|
|
209
|
-
};
|
package/src/oxlint.test.ts
DELETED
|
@@ -1,61 +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
|
-
import { describe, it, expect } from 'vitest';
|
|
8
|
-
import * as fs from 'node:fs';
|
|
9
|
-
import * as path from 'node:path';
|
|
10
|
-
import { plugin as mainPlugin } from './index';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Smoke test for the `./oxlint` sub-export.
|
|
14
|
-
*
|
|
15
|
-
* Runtime shape: `oxlint.ts` uses `export = plugin`, which compiles to
|
|
16
|
-
* `module.exports = plugin`. From an ESM dynamic `import('./oxlint')` view,
|
|
17
|
-
* Node's CJS interop surfaces that value at `.default`. From oxlint's
|
|
18
|
-
* `require('eslint-plugin-jwt-security/oxlint')` view, it sits at the top of
|
|
19
|
-
* `module.exports` — exactly the `{ meta, rules }` shape oxlint's JS plugin
|
|
20
|
-
* loader expects.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
type Plugin = typeof mainPlugin;
|
|
24
|
-
|
|
25
|
-
describe('eslint-plugin-jwt-security/oxlint sub-export', () => {
|
|
26
|
-
it('declares the ./oxlint sub-export in package.json', () => {
|
|
27
|
-
const pkgJson = JSON.parse(
|
|
28
|
-
fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'),
|
|
29
|
-
);
|
|
30
|
-
expect(pkgJson.exports['./oxlint']).toEqual({
|
|
31
|
-
types: './src/oxlint.d.ts',
|
|
32
|
-
default: './src/oxlint.js',
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('re-exports the plugin object (meta + rules at top level)', async () => {
|
|
37
|
-
const oxlintModule = await import('./oxlint.js');
|
|
38
|
-
const oxlintPlugin = (oxlintModule as unknown as { default: Plugin }).default;
|
|
39
|
-
expect(oxlintPlugin).toBeDefined();
|
|
40
|
-
expect(oxlintPlugin.meta?.name).toBe('eslint-plugin-jwt-security');
|
|
41
|
-
expect(oxlintPlugin.rules).toBeDefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('exposes the same rule names as the main entry (no rules dropped)', async () => {
|
|
45
|
-
const oxlintModule = await import('./oxlint.js');
|
|
46
|
-
const oxlintPlugin = (oxlintModule as unknown as { default: Plugin }).default;
|
|
47
|
-
expect(Object.keys(oxlintPlugin.rules || {}).toSorted()).toEqual(
|
|
48
|
-
Object.keys(mainPlugin.rules || {}).toSorted(),
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('exposes the same rule references (pass-through, not a copy)', async () => {
|
|
53
|
-
const oxlintModule = await import('./oxlint.js');
|
|
54
|
-
const oxlintPlugin = (oxlintModule as unknown as { default: Plugin }).default;
|
|
55
|
-
for (const ruleName of Object.keys(mainPlugin.rules || {})) {
|
|
56
|
-
expect(oxlintPlugin.rules?.[ruleName]).toBe(
|
|
57
|
-
(mainPlugin.rules as Record<string, unknown>)[ruleName],
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
});
|
package/src/oxlint.ts
DELETED
|
@@ -1,23 +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
|
-
* oxlint sub-export.
|
|
9
|
-
*
|
|
10
|
-
* Consumers wire oxlint to this entry via:
|
|
11
|
-
* { "jsPlugins": ["eslint-plugin-jwt-security/oxlint"] }
|
|
12
|
-
*
|
|
13
|
-
* oxlint's JS plugin loader does `require('eslint-plugin-jwt-security/oxlint')` and reads
|
|
14
|
-
* `.rules` off the result. The compiled output of `export = plugin` is
|
|
15
|
-
* `module.exports = plugin`, which gives oxlint the plugin object directly
|
|
16
|
-
* (`{ meta, rules }`) — no wrapper, no `.default` indirection.
|
|
17
|
-
*
|
|
18
|
-
* Generated by scripts/generate-oxlint-shims.mjs. Do not edit by hand.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { plugin } from './index';
|
|
22
|
-
|
|
23
|
-
export = plugin;
|