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,97 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,123 +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-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;
|
|
@@ -1,157 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,118 +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-timestamp-manipulation
|
|
9
|
-
*
|
|
10
|
-
* Detects patterns that disable automatic timestamp (iat) generation.
|
|
11
|
-
* This enables the "Back to the Future" replay attack (LightSEC 2025).
|
|
12
|
-
*
|
|
13
|
-
* CWE-294: Authentication Bypass by Capture-replay
|
|
14
|
-
*
|
|
15
|
-
* @see https://securitypattern.com/post/jwt-back-to-the-future
|
|
16
|
-
*/
|
|
17
|
-
import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
|
|
18
|
-
import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
|
|
19
|
-
import { isSignOperation, getOptionsArgument, hasOption, getOptionValue } from '../../utils';
|
|
20
|
-
import type { JwtRuleOptions } from '../../types';
|
|
21
|
-
|
|
22
|
-
type MessageIds = 'timestampDisabled' | 'noTimestampTrue';
|
|
23
|
-
|
|
24
|
-
type RuleOptions = [JwtRuleOptions?];
|
|
25
|
-
|
|
26
|
-
export const noTimestampManipulation = createRule<RuleOptions, MessageIds>({
|
|
27
|
-
name: 'no-timestamp-manipulation',
|
|
28
|
-
meta: {
|
|
29
|
-
type: 'problem',
|
|
30
|
-
docs: {
|
|
31
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-timestamp-manipulation.md',
|
|
32
|
-
description:
|
|
33
|
-
'Prevent disabling automatic timestamp generation which enables replay attacks',
|
|
34
|
-
cwe: 'CWE-294',
|
|
35
|
-
cvss: 7.5,
|
|
36
|
-
},
|
|
37
|
-
fixable: undefined,
|
|
38
|
-
hasSuggestions: false,
|
|
39
|
-
messages: {
|
|
40
|
-
timestampDisabled: formatLLMMessage({
|
|
41
|
-
icon: MessageIcons.SECURITY,
|
|
42
|
-
issueName: 'Timestamp Generation Disabled',
|
|
43
|
-
cwe: 'CWE-294',
|
|
44
|
-
description:
|
|
45
|
-
'noTimestamp:true disables iat claim, enabling replay attacks',
|
|
46
|
-
severity: 'HIGH',
|
|
47
|
-
fix: 'Remove noTimestamp option or set to false',
|
|
48
|
-
documentationLink: 'https://securitypattern.com/post/jwt-back-to-the-future',
|
|
49
|
-
}),
|
|
50
|
-
noTimestampTrue: formatLLMMessage({
|
|
51
|
-
icon: MessageIcons.SECURITY,
|
|
52
|
-
issueName: 'Back to the Future Attack Vector',
|
|
53
|
-
cwe: 'CWE-294',
|
|
54
|
-
description:
|
|
55
|
-
'Disabling timestamps allows attackers to forge tokens valid in the future',
|
|
56
|
-
severity: 'HIGH',
|
|
57
|
-
fix: 'Always include iat claim for freshness validation',
|
|
58
|
-
documentationLink: 'https://securitypattern.com/post/jwt-back-to-the-future',
|
|
59
|
-
}),
|
|
60
|
-
},
|
|
61
|
-
schema: [
|
|
62
|
-
{
|
|
63
|
-
type: 'object',
|
|
64
|
-
properties: {
|
|
65
|
-
trustedSanitizers: {
|
|
66
|
-
type: 'array',
|
|
67
|
-
items: { type: 'string' },
|
|
68
|
-
default: [],
|
|
69
|
-
},
|
|
70
|
-
trustedAnnotations: {
|
|
71
|
-
type: 'array',
|
|
72
|
-
items: { type: 'string' },
|
|
73
|
-
default: [],
|
|
74
|
-
},
|
|
75
|
-
strictMode: {
|
|
76
|
-
type: 'boolean',
|
|
77
|
-
default: false,
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
additionalProperties: false,
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
},
|
|
84
|
-
defaultOptions: [
|
|
85
|
-
{
|
|
86
|
-
trustedSanitizers: [],
|
|
87
|
-
trustedAnnotations: [],
|
|
88
|
-
strictMode: false,
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
|
|
92
|
-
return {
|
|
93
|
-
CallExpression(node: TSESTree.CallExpression) {
|
|
94
|
-
if (!isSignOperation(node)) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const optionsArg = getOptionsArgument(node, 2);
|
|
99
|
-
if (!optionsArg) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Check for noTimestamp: true
|
|
104
|
-
if (hasOption(optionsArg, 'noTimestamp')) {
|
|
105
|
-
const value = getOptionValue(optionsArg, 'noTimestamp');
|
|
106
|
-
if (value?.type === 'Literal' && value.value === true) {
|
|
107
|
-
context.report({
|
|
108
|
-
node: value,
|
|
109
|
-
messageId: 'noTimestampTrue',
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
export default noTimestampManipulation;
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for no-timestamp-manipulation rule
|
|
3
|
-
* Security: LightSEC 2025 - "Back to the Future" Attack
|
|
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 { noTimestampManipulation } 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-timestamp-manipulation', () => {
|
|
24
|
-
describe('Valid Code', () => {
|
|
25
|
-
ruleTester.run('valid - default timestamps', noTimestampManipulation, {
|
|
26
|
-
valid: [
|
|
27
|
-
'const x = 42;',
|
|
28
|
-
'const flag = true;',
|
|
29
|
-
{ code: `jwt.sign(payload, secret);` },
|
|
30
|
-
{ code: `jwt.sign(payload, secret, {});` },
|
|
31
|
-
{ code: `jwt.sign(payload, secret, { expiresIn: '1h' });` },
|
|
32
|
-
{ code: `jwt.sign(payload, secret, { noTimestamp: false });` },
|
|
33
|
-
{ code: `jwt.verify(token, secret);` }, // verify not checked
|
|
34
|
-
],
|
|
35
|
-
invalid: [],
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('Invalid Code', () => {
|
|
40
|
-
ruleTester.run('invalid - noTimestamp true', noTimestampManipulation, {
|
|
41
|
-
valid: [],
|
|
42
|
-
invalid: [
|
|
43
|
-
{
|
|
44
|
-
code: `jwt.sign(payload, secret, { noTimestamp: true });`,
|
|
45
|
-
errors: [{ messageId: 'noTimestampTrue' }],
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
code: `jwt.sign(payload, secret, { algorithm: 'RS256', noTimestamp: true });`,
|
|
49
|
-
errors: [{ messageId: 'noTimestampTrue' }],
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
});
|