eslint-plugin-jwt-security 2.3.0 → 2.3.2

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.
Files changed (97) hide show
  1. package/README.md +5 -5
  2. package/package.json +16 -24
  3. package/src/index.d.ts +17 -0
  4. package/src/index.js +1 -0
  5. package/src/oxlint.d.ts +17 -0
  6. package/src/oxlint.js +1 -0
  7. package/src/rules/no-algorithm-confusion/index.js +1 -0
  8. package/src/rules/no-algorithm-none/index.js +1 -0
  9. package/src/rules/no-decode-without-verify/index.js +1 -0
  10. package/src/rules/no-hardcoded-secret/index.js +1 -0
  11. package/src/rules/no-sensitive-payload/index.js +1 -0
  12. package/src/rules/no-timestamp-manipulation/index.js +1 -0
  13. package/src/rules/no-weak-secret/index.js +1 -0
  14. package/src/rules/require-algorithm-whitelist/index.js +1 -0
  15. package/src/rules/require-audience-validation/index.js +1 -0
  16. package/src/rules/require-expiration/index.js +1 -0
  17. package/src/rules/require-issued-at/index.js +1 -0
  18. package/src/rules/require-issuer-validation/index.js +1 -0
  19. package/src/rules/require-max-age/index.js +1 -0
  20. package/src/types/index.js +1 -0
  21. package/src/utils/index.js +1 -0
  22. package/AGENTS.md +0 -102
  23. package/CHANGELOG.md +0 -161
  24. package/dist/AGENTS.md +0 -102
  25. package/dist/CHANGELOG.md +0 -161
  26. package/dist/LICENSE +0 -23
  27. package/dist/README.md +0 -158
  28. package/dist/package.json +0 -125
  29. package/dist/src/index.d.ts +0 -101
  30. package/dist/src/index.js +0 -171
  31. package/dist/src/oxlint.d.ts +0 -20
  32. package/dist/src/oxlint.js +0 -21
  33. package/dist/src/rules/no-algorithm-confusion/index.d.ts +0 -24
  34. package/dist/src/rules/no-algorithm-confusion/index.js +0 -165
  35. package/dist/src/rules/no-algorithm-none/index.d.ts +0 -24
  36. package/dist/src/rules/no-algorithm-none/index.js +0 -222
  37. package/dist/src/rules/no-decode-without-verify/index.d.ts +0 -23
  38. package/dist/src/rules/no-decode-without-verify/index.js +0 -131
  39. package/dist/src/rules/no-hardcoded-secret/index.d.ts +0 -23
  40. package/dist/src/rules/no-hardcoded-secret/index.js +0 -189
  41. package/dist/src/rules/no-sensitive-payload/index.d.ts +0 -20
  42. package/dist/src/rules/no-sensitive-payload/index.js +0 -103
  43. package/dist/src/rules/no-timestamp-manipulation/index.d.ts +0 -23
  44. package/dist/src/rules/no-timestamp-manipulation/index.js +0 -97
  45. package/dist/src/rules/no-weak-secret/index.d.ts +0 -23
  46. package/dist/src/rules/no-weak-secret/index.js +0 -165
  47. package/dist/src/rules/require-algorithm-whitelist/index.d.ts +0 -24
  48. package/dist/src/rules/require-algorithm-whitelist/index.js +0 -113
  49. package/dist/src/rules/require-audience-validation/index.d.ts +0 -20
  50. package/dist/src/rules/require-audience-validation/index.js +0 -100
  51. package/dist/src/rules/require-expiration/index.d.ts +0 -23
  52. package/dist/src/rules/require-expiration/index.js +0 -136
  53. package/dist/src/rules/require-issued-at/index.d.ts +0 -23
  54. package/dist/src/rules/require-issued-at/index.js +0 -113
  55. package/dist/src/rules/require-issuer-validation/index.d.ts +0 -20
  56. package/dist/src/rules/require-issuer-validation/index.js +0 -100
  57. package/dist/src/rules/require-max-age/index.d.ts +0 -24
  58. package/dist/src/rules/require-max-age/index.js +0 -93
  59. package/dist/src/types/index.js +0 -7
  60. package/dist/src/utils/index.d.ts +0 -98
  61. package/dist/src/utils/index.js +0 -238
  62. package/src/__compatibility__/jwt-interface.spec.ts +0 -192
  63. package/src/index.test.ts +0 -47
  64. package/src/index.ts +0 -209
  65. package/src/oxlint.test.ts +0 -61
  66. package/src/oxlint.ts +0 -23
  67. package/src/rules/no-algorithm-confusion/index.ts +0 -208
  68. package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +0 -106
  69. package/src/rules/no-algorithm-none/index.ts +0 -270
  70. package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +0 -146
  71. package/src/rules/no-decode-without-verify/index.ts +0 -162
  72. package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +0 -76
  73. package/src/rules/no-hardcoded-secret/index.ts +0 -230
  74. package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +0 -97
  75. package/src/rules/no-sensitive-payload/index.ts +0 -123
  76. package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +0 -157
  77. package/src/rules/no-timestamp-manipulation/index.ts +0 -118
  78. package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +0 -54
  79. package/src/rules/no-weak-secret/index.ts +0 -204
  80. package/src/rules/no-weak-secret/no-weak-secret.test.ts +0 -114
  81. package/src/rules/require-algorithm-whitelist/index.ts +0 -145
  82. package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +0 -72
  83. package/src/rules/require-audience-validation/index.ts +0 -120
  84. package/src/rules/require-audience-validation/require-audience-validation.test.ts +0 -68
  85. package/src/rules/require-expiration/index.ts +0 -170
  86. package/src/rules/require-expiration/require-expiration.test.ts +0 -135
  87. package/src/rules/require-issued-at/index.ts +0 -143
  88. package/src/rules/require-issued-at/require-issued-at.test.ts +0 -64
  89. package/src/rules/require-issuer-validation/index.ts +0 -120
  90. package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +0 -68
  91. package/src/rules/require-max-age/index.ts +0 -117
  92. package/src/rules/require-max-age/require-max-age.test.ts +0 -69
  93. package/src/tests/coverage-dual-layer.test.ts +0 -323
  94. package/src/types/index.ts +0 -107
  95. package/src/utils/index.ts +0 -285
  96. package/src/utils/utils.test.ts +0 -324
  97. /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
- });