eslint-plugin-jwt-security 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/AGENTS.md +102 -0
  2. package/CHANGELOG.md +161 -0
  3. package/LICENSE +23 -0
  4. package/README.md +158 -0
  5. package/dist/AGENTS.md +102 -0
  6. package/dist/CHANGELOG.md +161 -0
  7. package/dist/LICENSE +23 -0
  8. package/dist/README.md +158 -0
  9. package/dist/package.json +125 -0
  10. package/dist/src/index.d.ts +101 -0
  11. package/dist/src/index.js +171 -0
  12. package/dist/src/oxlint.d.ts +20 -0
  13. package/dist/src/oxlint.js +21 -0
  14. package/dist/src/rules/no-algorithm-confusion/index.d.ts +24 -0
  15. package/dist/src/rules/no-algorithm-confusion/index.js +165 -0
  16. package/dist/src/rules/no-algorithm-none/index.d.ts +24 -0
  17. package/dist/src/rules/no-algorithm-none/index.js +222 -0
  18. package/dist/src/rules/no-decode-without-verify/index.d.ts +23 -0
  19. package/dist/src/rules/no-decode-without-verify/index.js +131 -0
  20. package/dist/src/rules/no-hardcoded-secret/index.d.ts +23 -0
  21. package/dist/src/rules/no-hardcoded-secret/index.js +189 -0
  22. package/dist/src/rules/no-sensitive-payload/index.d.ts +20 -0
  23. package/dist/src/rules/no-sensitive-payload/index.js +103 -0
  24. package/dist/src/rules/no-timestamp-manipulation/index.d.ts +23 -0
  25. package/dist/src/rules/no-timestamp-manipulation/index.js +97 -0
  26. package/dist/src/rules/no-weak-secret/index.d.ts +23 -0
  27. package/dist/src/rules/no-weak-secret/index.js +165 -0
  28. package/dist/src/rules/require-algorithm-whitelist/index.d.ts +24 -0
  29. package/dist/src/rules/require-algorithm-whitelist/index.js +113 -0
  30. package/dist/src/rules/require-audience-validation/index.d.ts +20 -0
  31. package/dist/src/rules/require-audience-validation/index.js +100 -0
  32. package/dist/src/rules/require-expiration/index.d.ts +23 -0
  33. package/dist/src/rules/require-expiration/index.js +136 -0
  34. package/dist/src/rules/require-issued-at/index.d.ts +23 -0
  35. package/dist/src/rules/require-issued-at/index.js +113 -0
  36. package/dist/src/rules/require-issuer-validation/index.d.ts +20 -0
  37. package/dist/src/rules/require-issuer-validation/index.js +100 -0
  38. package/dist/src/rules/require-max-age/index.d.ts +24 -0
  39. package/dist/src/rules/require-max-age/index.js +93 -0
  40. package/dist/src/types/index.d.ts +82 -0
  41. package/dist/src/types/index.js +7 -0
  42. package/dist/src/utils/index.d.ts +98 -0
  43. package/dist/src/utils/index.js +238 -0
  44. package/package.json +125 -0
  45. package/src/__compatibility__/jwt-interface.spec.ts +192 -0
  46. package/src/index.test.ts +47 -0
  47. package/src/index.ts +209 -0
  48. package/src/oxlint.test.ts +61 -0
  49. package/src/oxlint.ts +23 -0
  50. package/src/rules/no-algorithm-confusion/index.ts +208 -0
  51. package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +106 -0
  52. package/src/rules/no-algorithm-none/index.ts +270 -0
  53. package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +146 -0
  54. package/src/rules/no-decode-without-verify/index.ts +162 -0
  55. package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +76 -0
  56. package/src/rules/no-hardcoded-secret/index.ts +230 -0
  57. package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +97 -0
  58. package/src/rules/no-sensitive-payload/index.ts +123 -0
  59. package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +157 -0
  60. package/src/rules/no-timestamp-manipulation/index.ts +118 -0
  61. package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +54 -0
  62. package/src/rules/no-weak-secret/index.ts +204 -0
  63. package/src/rules/no-weak-secret/no-weak-secret.test.ts +114 -0
  64. package/src/rules/require-algorithm-whitelist/index.ts +145 -0
  65. package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +72 -0
  66. package/src/rules/require-audience-validation/index.ts +120 -0
  67. package/src/rules/require-audience-validation/require-audience-validation.test.ts +68 -0
  68. package/src/rules/require-expiration/index.ts +170 -0
  69. package/src/rules/require-expiration/require-expiration.test.ts +135 -0
  70. package/src/rules/require-issued-at/index.ts +143 -0
  71. package/src/rules/require-issued-at/require-issued-at.test.ts +64 -0
  72. package/src/rules/require-issuer-validation/index.ts +120 -0
  73. package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +68 -0
  74. package/src/rules/require-max-age/index.ts +117 -0
  75. package/src/rules/require-max-age/require-max-age.test.ts +69 -0
  76. package/src/tests/coverage-dual-layer.test.ts +323 -0
  77. package/src/types/index.ts +107 -0
  78. package/src/utils/index.ts +285 -0
  79. package/src/utils/utils.test.ts +324 -0
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2025 Ofri Peretz
4
+ * Licensed under the MIT License. Use of this source code is governed by the
5
+ * MIT license that can be found in the LICENSE file.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noDecodeWithoutVerify = void 0;
9
+ const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ const utils_1 = require("../../utils");
11
+ exports.noDecodeWithoutVerify = (0, eslint_devkit_1.createRule)({
12
+ name: 'no-decode-without-verify',
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-decode-without-verify.md',
17
+ description: 'Disallow trusting decoded JWT payload without signature verification',
18
+ cwe: 'CWE-345',
19
+ cvss: 7.5,
20
+ },
21
+ fixable: undefined,
22
+ hasSuggestions: false,
23
+ messages: {
24
+ decodeWithoutVerify: (0, eslint_devkit_1.formatLLMMessage)({
25
+ icon: eslint_devkit_1.MessageIcons.SECURITY,
26
+ issueName: 'Decoded JWT Without Verification',
27
+ cwe: 'CWE-345',
28
+ description: 'jwt.decode() returns payload without verifying signature - data can be forged',
29
+ severity: 'HIGH',
30
+ fix: 'Use jwt.verify(token, secret) instead of jwt.decode(token)',
31
+ documentationLink: 'https://owasp.org/API-Security/0xa7-security-misconfiguration/',
32
+ }),
33
+ jwtDecodeLibrary: (0, eslint_devkit_1.formatLLMMessage)({
34
+ icon: eslint_devkit_1.MessageIcons.SECURITY,
35
+ issueName: 'jwt-decode Library Usage',
36
+ cwe: 'CWE-345',
37
+ description: 'jwt-decode library only decodes tokens, never verifies signatures',
38
+ severity: 'HIGH',
39
+ fix: 'Use jsonwebtoken.verify() or jose.jwtVerify() for verification',
40
+ documentationLink: 'https://www.npmjs.com/package/jwt-decode',
41
+ }),
42
+ useVerifyInstead: (0, eslint_devkit_1.formatLLMMessage)({
43
+ icon: eslint_devkit_1.MessageIcons.INFO,
44
+ issueName: 'Use Verify Instead',
45
+ description: 'Replace decode with verify to ensure authenticity',
46
+ severity: 'LOW',
47
+ fix: 'jwt.verify(token, secret, { algorithms: ["RS256"] })',
48
+ documentationLink: 'https://www.npmjs.com/package/jsonwebtoken',
49
+ }),
50
+ },
51
+ schema: [
52
+ {
53
+ type: 'object',
54
+ properties: {
55
+ allowHeaderInspection: {
56
+ type: 'boolean',
57
+ default: false,
58
+ description: 'Allow decode() for reading header before verification',
59
+ },
60
+ trustedSanitizers: {
61
+ type: 'array',
62
+ items: { type: 'string' },
63
+ default: [],
64
+ },
65
+ trustedAnnotations: {
66
+ type: 'array',
67
+ items: { type: 'string' },
68
+ default: ['@decoded-header-only', '@verified-separately'],
69
+ },
70
+ strictMode: {
71
+ type: 'boolean',
72
+ default: false,
73
+ },
74
+ },
75
+ additionalProperties: false,
76
+ },
77
+ ],
78
+ },
79
+ defaultOptions: [
80
+ {
81
+ allowHeaderInspection: false,
82
+ trustedSanitizers: [],
83
+ trustedAnnotations: ['@decoded-header-only', '@verified-separately'],
84
+ strictMode: false,
85
+ },
86
+ ],
87
+ create(context) {
88
+ const options = context.options[0] ?? {};
89
+ const { trustedAnnotations = [] } = options;
90
+ /**
91
+ * Check if this is a jwt-decode import usage
92
+ */
93
+ // oxlint-disable-next-line consistent-function-scoping
94
+ const isJwtDecodeLibrary = (node) => {
95
+ // Direct call: jwtDecode(token)
96
+ if (node.callee.type === 'Identifier' &&
97
+ (node.callee.name === 'jwtDecode' || node.callee.name === 'jwt_decode')) {
98
+ return true;
99
+ }
100
+ return false;
101
+ };
102
+ return {
103
+ CallExpression(node) {
104
+ // Check for jwt.decode() pattern
105
+ if ((0, utils_1.isDecodeOperation)(node)) {
106
+ // Check for safe annotations
107
+ if ((0, eslint_devkit_1.hasSafeAnnotation)(node, context, trustedAnnotations)) {
108
+ return;
109
+ }
110
+ context.report({
111
+ node,
112
+ messageId: 'decodeWithoutVerify',
113
+ });
114
+ return;
115
+ }
116
+ // Check for jwt-decode library usage
117
+ if (isJwtDecodeLibrary(node)) {
118
+ // Check for safe annotations
119
+ if ((0, eslint_devkit_1.hasSafeAnnotation)(node, context, trustedAnnotations)) {
120
+ return;
121
+ }
122
+ context.report({
123
+ node,
124
+ messageId: 'jwtDecodeLibrary',
125
+ });
126
+ }
127
+ },
128
+ };
129
+ },
130
+ });
131
+ exports.default = exports.noDecodeWithoutVerify;
@@ -0,0 +1,23 @@
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
+ * ESLint Rule: no-hardcoded-secret
8
+ *
9
+ * Detects hardcoded secrets in JWT sign/verify operations.
10
+ * Hardcoded secrets in source code are a critical security vulnerability.
11
+ *
12
+ * CWE-798: Use of Hard-coded Credentials
13
+ *
14
+ * @see https://cwe.mitre.org/data/definitions/798.html
15
+ */
16
+ import type { TSESLint } from '@interlace/eslint-devkit';
17
+ import type { NoHardcodedSecretOptions } from '../../types';
18
+ type MessageIds = 'hardcodedSecret' | 'useEnvVariable';
19
+ type RuleOptions = [NoHardcodedSecretOptions?];
20
+ export declare const noHardcodedSecret: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
21
+ name: string;
22
+ };
23
+ export default noHardcodedSecret;
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2025 Ofri Peretz
4
+ * Licensed under the MIT License. Use of this source code is governed by the
5
+ * MIT license that can be found in the LICENSE file.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noHardcodedSecret = void 0;
9
+ const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ const utils_1 = require("../../utils");
11
+ exports.noHardcodedSecret = (0, eslint_devkit_1.createRule)({
12
+ name: 'no-hardcoded-secret',
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-hardcoded-secret.md',
17
+ description: 'Disallow hardcoded secrets in JWT sign/verify operations',
18
+ cwe: 'CWE-798',
19
+ cvss: 9.8,
20
+ },
21
+ fixable: undefined,
22
+ hasSuggestions: false,
23
+ messages: {
24
+ hardcodedSecret: (0, eslint_devkit_1.formatLLMMessage)({
25
+ icon: eslint_devkit_1.MessageIcons.SECURITY,
26
+ issueName: 'Hardcoded JWT Secret',
27
+ cwe: 'CWE-798',
28
+ description: 'Secret key hardcoded in source code can be extracted from repositories',
29
+ severity: 'HIGH',
30
+ fix: 'Use process.env.JWT_SECRET or secure secret management',
31
+ documentationLink: 'https://cwe.mitre.org/data/definitions/798.html',
32
+ }),
33
+ useEnvVariable: (0, eslint_devkit_1.formatLLMMessage)({
34
+ icon: eslint_devkit_1.MessageIcons.INFO,
35
+ issueName: 'Use Environment Variable',
36
+ description: 'Replace hardcoded secret with environment variable',
37
+ severity: 'LOW',
38
+ fix: 'process.env.JWT_SECRET',
39
+ documentationLink: 'https://12factor.net/config',
40
+ }),
41
+ },
42
+ schema: [
43
+ {
44
+ type: 'object',
45
+ properties: {
46
+ envPatterns: {
47
+ type: 'array',
48
+ items: { type: 'string' },
49
+ default: [],
50
+ description: 'Patterns that indicate safe environment variable usage',
51
+ },
52
+ trustedSanitizers: {
53
+ type: 'array',
54
+ items: { type: 'string' },
55
+ default: [],
56
+ },
57
+ trustedAnnotations: {
58
+ type: 'array',
59
+ items: { type: 'string' },
60
+ default: [],
61
+ },
62
+ strictMode: {
63
+ type: 'boolean',
64
+ default: false,
65
+ },
66
+ },
67
+ additionalProperties: false,
68
+ },
69
+ ],
70
+ },
71
+ defaultOptions: [
72
+ {
73
+ envPatterns: [],
74
+ trustedSanitizers: [],
75
+ trustedAnnotations: [],
76
+ strictMode: false,
77
+ },
78
+ ],
79
+ create(context) {
80
+ /**
81
+ * Check if a node is a hardcoded string literal
82
+ */
83
+ // oxlint-disable-next-line consistent-function-scoping
84
+ const isHardcodedString = (node) => {
85
+ // String literal
86
+ if (node.type === 'Literal' && typeof node.value === 'string') {
87
+ return true;
88
+ }
89
+ // Template literal without expressions
90
+ if (node.type === 'TemplateLiteral' &&
91
+ node.expressions.length === 0) {
92
+ return true;
93
+ }
94
+ return false;
95
+ };
96
+ /**
97
+ * Resolve an Identifier node to its initializer (one frame of indirection).
98
+ * Closes the audit FN where `const SECRET = 'x'; jwt.sign(p, SECRET)`
99
+ * was treated as safe — the literal-via-const pattern hides the
100
+ * hardcoded secret behind one declaration. See benchmarks/AUDIT_PATTERNS.md
101
+ * §3.2 ("Indirection through one level of variable").
102
+ */
103
+ const resolveConstLiteralValue = (node) => {
104
+ if (node.type !== 'Identifier')
105
+ return null;
106
+ const scope = context.sourceCode.getScope?.(node) ?? null;
107
+ if (!scope)
108
+ return null;
109
+ const variable = scope.references.find((r) => r.identifier === node)?.resolved;
110
+ const def = variable?.defs[0];
111
+ if (!def || def.type !== 'Variable')
112
+ return null;
113
+ const decl = def.parent;
114
+ if (decl?.type !== 'VariableDeclaration' || decl.kind !== 'const')
115
+ return null;
116
+ return def.node.init ?? null;
117
+ };
118
+ /**
119
+ * Check if node is a safe key source
120
+ */
121
+ const isSafeKeySource = (node) => {
122
+ // Environment variable
123
+ if ((0, utils_1.isEnvVariable)(node)) {
124
+ return true;
125
+ }
126
+ // Function call (getSecret(), loadKey(), etc.)
127
+ if (node.type === 'CallExpression') {
128
+ return true;
129
+ }
130
+ // await expression (async key loading)
131
+ if (node.type === 'AwaitExpression') {
132
+ return true;
133
+ }
134
+ // Variable reference: resolve `const X = ...` one level. If X is a
135
+ // hardcoded literal, treat as unsafe — fall through to the
136
+ // hardcoded check. Otherwise (env var, call, identifier chain)
137
+ // it's safe.
138
+ if (node.type === 'Identifier') {
139
+ const init = resolveConstLiteralValue(node);
140
+ if (init && isHardcodedString(init)) {
141
+ // Unsafe — let the caller's `isHardcodedString` (with the
142
+ // resolved init) flag it.
143
+ return false;
144
+ }
145
+ return true;
146
+ }
147
+ // Member expression but not literal (config.secret, etc.)
148
+ if (node.type === 'MemberExpression' && !isHardcodedString(node)) {
149
+ return true;
150
+ }
151
+ return false;
152
+ };
153
+ /**
154
+ * Like isHardcodedString but resolves single-level `const X = '...'`
155
+ * indirection so the caller fires on `const SECRET = 'x'; jwt.sign(p, SECRET)`.
156
+ */
157
+ const isHardcodedStringOrResolvedConst = (node) => {
158
+ if (isHardcodedString(node))
159
+ return true;
160
+ const init = resolveConstLiteralValue(node);
161
+ return init ? isHardcodedString(init) : false;
162
+ };
163
+ return {
164
+ CallExpression(node) {
165
+ // Check both sign and verify operations
166
+ if (!(0, utils_1.isSignOperation)(node) && !(0, utils_1.isVerifyOperation)(node)) {
167
+ return;
168
+ }
169
+ // Secret/key is the second argument
170
+ if (node.arguments.length < 2) {
171
+ return;
172
+ }
173
+ const secretArg = node.arguments[1];
174
+ // Skip if safe source
175
+ if (isSafeKeySource(secretArg)) {
176
+ return;
177
+ }
178
+ // Flag hardcoded strings (also follows single-frame `const X = '...'`).
179
+ if (isHardcodedStringOrResolvedConst(secretArg)) {
180
+ context.report({
181
+ node: secretArg,
182
+ messageId: 'hardcodedSecret',
183
+ });
184
+ }
185
+ },
186
+ };
187
+ },
188
+ });
189
+ exports.default = exports.noHardcodedSecret;
@@ -0,0 +1,20 @@
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
+ * ESLint Rule: no-sensitive-payload
8
+ *
9
+ * Detects storage of sensitive data in JWT payload.
10
+ * JWT payloads are only base64-encoded, not encrypted.
11
+ *
12
+ * CWE-359: Exposure of Private Personal Information
13
+ */
14
+ import type { TSESLint } from '@interlace/eslint-devkit';
15
+ import type { NoSensitivePayloadOptions } from '../../types';
16
+ type RuleOptions = [NoSensitivePayloadOptions?];
17
+ export declare const noSensitivePayload: TSESLint.RuleModule<"sensitivePayloadField", RuleOptions, unknown, TSESLint.RuleListener> & {
18
+ name: string;
19
+ };
20
+ export default noSensitivePayload;
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2025 Ofri Peretz
4
+ * Licensed under the MIT License. Use of this source code is governed by the
5
+ * MIT license that can be found in the LICENSE file.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noSensitivePayload = void 0;
9
+ const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ const utils_1 = require("../../utils");
11
+ exports.noSensitivePayload = (0, eslint_devkit_1.createRule)({
12
+ name: 'no-sensitive-payload',
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-sensitive-payload.md',
17
+ description: 'Prevent storing sensitive data in JWT payload which is only base64-encoded',
18
+ cwe: 'CWE-359',
19
+ cvss: 5,
20
+ },
21
+ fixable: undefined,
22
+ hasSuggestions: false,
23
+ messages: {
24
+ sensitivePayloadField: (0, eslint_devkit_1.formatLLMMessage)({
25
+ icon: eslint_devkit_1.MessageIcons.SECURITY,
26
+ issueName: 'Sensitive Data in JWT Payload',
27
+ cwe: 'CWE-359',
28
+ description: 'JWT payloads are not encrypted - sensitive data like "{{fieldName}}" can be read by anyone',
29
+ severity: 'MEDIUM',
30
+ fix: 'Store sensitive data server-side, reference by ID in token',
31
+ documentationLink: 'https://tools.ietf.org/html/rfc8725',
32
+ }),
33
+ },
34
+ schema: [
35
+ {
36
+ type: 'object',
37
+ properties: {
38
+ additionalSensitiveFields: {
39
+ type: 'array',
40
+ items: { type: 'string' },
41
+ default: [],
42
+ description: 'Additional field names to flag as sensitive',
43
+ },
44
+ trustedSanitizers: {
45
+ type: 'array',
46
+ items: { type: 'string' },
47
+ default: [],
48
+ },
49
+ trustedAnnotations: {
50
+ type: 'array',
51
+ items: { type: 'string' },
52
+ default: [],
53
+ },
54
+ strictMode: {
55
+ type: 'boolean',
56
+ default: false,
57
+ },
58
+ },
59
+ additionalProperties: false,
60
+ },
61
+ ],
62
+ },
63
+ defaultOptions: [
64
+ {
65
+ additionalSensitiveFields: [],
66
+ trustedSanitizers: [],
67
+ trustedAnnotations: [],
68
+ strictMode: false,
69
+ },
70
+ ],
71
+ create(context) {
72
+ const options = context.options[0] ?? {};
73
+ const additionalFields = options.additionalSensitiveFields ?? [];
74
+ const allSensitiveFields = new Set([...utils_1.SENSITIVE_PAYLOAD_FIELDS, ...additionalFields]);
75
+ return {
76
+ CallExpression(node) {
77
+ if (!(0, utils_1.isSignOperation)(node)) {
78
+ return;
79
+ }
80
+ if (node.arguments.length < 1) {
81
+ return;
82
+ }
83
+ const payloadArg = node.arguments[0];
84
+ // Check object literal payload
85
+ if (payloadArg.type === 'ObjectExpression') {
86
+ for (const prop of payloadArg.properties) {
87
+ if (prop.type === 'Property' && prop.key.type === 'Identifier') {
88
+ const fieldName = prop.key.name.toLowerCase();
89
+ if (allSensitiveFields.has(fieldName)) {
90
+ context.report({
91
+ node: prop,
92
+ messageId: 'sensitivePayloadField',
93
+ data: { fieldName: prop.key.name },
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+ },
100
+ };
101
+ },
102
+ });
103
+ exports.default = exports.noSensitivePayload;
@@ -0,0 +1,23 @@
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
+ * ESLint Rule: no-timestamp-manipulation
8
+ *
9
+ * Detects patterns that disable automatic timestamp (iat) generation.
10
+ * This enables the "Back to the Future" replay attack (LightSEC 2025).
11
+ *
12
+ * CWE-294: Authentication Bypass by Capture-replay
13
+ *
14
+ * @see https://securitypattern.com/post/jwt-back-to-the-future
15
+ */
16
+ import type { TSESLint } from '@interlace/eslint-devkit';
17
+ import type { JwtRuleOptions } from '../../types';
18
+ type MessageIds = 'timestampDisabled' | 'noTimestampTrue';
19
+ type RuleOptions = [JwtRuleOptions?];
20
+ export declare const noTimestampManipulation: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
21
+ name: string;
22
+ };
23
+ export default noTimestampManipulation;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2025 Ofri Peretz
4
+ * Licensed under the MIT License. Use of this source code is governed by the
5
+ * MIT license that can be found in the LICENSE file.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.noTimestampManipulation = void 0;
9
+ const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ const utils_1 = require("../../utils");
11
+ exports.noTimestampManipulation = (0, eslint_devkit_1.createRule)({
12
+ name: 'no-timestamp-manipulation',
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-timestamp-manipulation.md',
17
+ description: 'Prevent disabling automatic timestamp generation which enables replay attacks',
18
+ cwe: 'CWE-294',
19
+ cvss: 7.5,
20
+ },
21
+ fixable: undefined,
22
+ hasSuggestions: false,
23
+ messages: {
24
+ timestampDisabled: (0, eslint_devkit_1.formatLLMMessage)({
25
+ icon: eslint_devkit_1.MessageIcons.SECURITY,
26
+ issueName: 'Timestamp Generation Disabled',
27
+ cwe: 'CWE-294',
28
+ description: 'noTimestamp:true disables iat claim, enabling replay attacks',
29
+ severity: 'HIGH',
30
+ fix: 'Remove noTimestamp option or set to false',
31
+ documentationLink: 'https://securitypattern.com/post/jwt-back-to-the-future',
32
+ }),
33
+ noTimestampTrue: (0, eslint_devkit_1.formatLLMMessage)({
34
+ icon: eslint_devkit_1.MessageIcons.SECURITY,
35
+ issueName: 'Back to the Future Attack Vector',
36
+ cwe: 'CWE-294',
37
+ description: 'Disabling timestamps allows attackers to forge tokens valid in the future',
38
+ severity: 'HIGH',
39
+ fix: 'Always include iat claim for freshness validation',
40
+ documentationLink: 'https://securitypattern.com/post/jwt-back-to-the-future',
41
+ }),
42
+ },
43
+ schema: [
44
+ {
45
+ type: 'object',
46
+ properties: {
47
+ trustedSanitizers: {
48
+ type: 'array',
49
+ items: { type: 'string' },
50
+ default: [],
51
+ },
52
+ trustedAnnotations: {
53
+ type: 'array',
54
+ items: { type: 'string' },
55
+ default: [],
56
+ },
57
+ strictMode: {
58
+ type: 'boolean',
59
+ default: false,
60
+ },
61
+ },
62
+ additionalProperties: false,
63
+ },
64
+ ],
65
+ },
66
+ defaultOptions: [
67
+ {
68
+ trustedSanitizers: [],
69
+ trustedAnnotations: [],
70
+ strictMode: false,
71
+ },
72
+ ],
73
+ create(context) {
74
+ return {
75
+ CallExpression(node) {
76
+ if (!(0, utils_1.isSignOperation)(node)) {
77
+ return;
78
+ }
79
+ const optionsArg = (0, utils_1.getOptionsArgument)(node, 2);
80
+ if (!optionsArg) {
81
+ return;
82
+ }
83
+ // Check for noTimestamp: true
84
+ if ((0, utils_1.hasOption)(optionsArg, 'noTimestamp')) {
85
+ const value = (0, utils_1.getOptionValue)(optionsArg, 'noTimestamp');
86
+ if (value?.type === 'Literal' && value.value === true) {
87
+ context.report({
88
+ node: value,
89
+ messageId: 'noTimestampTrue',
90
+ });
91
+ }
92
+ }
93
+ },
94
+ };
95
+ },
96
+ });
97
+ exports.default = exports.noTimestampManipulation;
@@ -0,0 +1,23 @@
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
+ * ESLint Rule: no-weak-secret
8
+ *
9
+ * Detects weak secrets used for HMAC-based JWT signing.
10
+ * Weak secrets can be brute-forced, allowing attackers to forge tokens.
11
+ *
12
+ * CWE-326: Inadequate Encryption Strength
13
+ *
14
+ * @see https://tools.ietf.org/html/rfc8725
15
+ */
16
+ import type { TSESLint } from '@interlace/eslint-devkit';
17
+ import type { NoWeakSecretOptions } from '../../types';
18
+ type MessageIds = 'weakSecret' | 'shortSecret' | 'useStrongSecret';
19
+ type RuleOptions = [NoWeakSecretOptions?];
20
+ export declare const noWeakSecret: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
21
+ name: string;
22
+ };
23
+ export default noWeakSecret;