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,146 +0,0 @@
1
- /**
2
- * Tests for no-algorithm-none rule
3
- * Security: CWE-347 (CVE-2022-23540)
4
- */
5
- import { RuleTester } from '@typescript-eslint/rule-tester';
6
- import { describe, it, afterAll } from 'vitest';
7
- import parser from '@typescript-eslint/parser';
8
- import { noAlgorithmNone } from './index';
9
-
10
- // Configure RuleTester for Vitest
11
- RuleTester.afterAll = afterAll;
12
- RuleTester.it = it;
13
- RuleTester.itOnly = it.only;
14
- RuleTester.describe = describe;
15
-
16
- const ruleTester = new RuleTester({
17
- languageOptions: {
18
- parser,
19
- ecmaVersion: 2022,
20
- sourceType: 'module',
21
- },
22
- });
23
-
24
- describe('no-algorithm-none', () => {
25
- describe('Valid Code - Secure Algorithms', () => {
26
- ruleTester.run('valid - secure algorithms', noAlgorithmNone, {
27
- valid: [
28
- // RS256 - RSA with SHA-256
29
- { code: `jwt.verify(token, publicKey, { algorithms: ['RS256'] });` },
30
- // ES256 - ECDSA with SHA-256
31
- { code: `jwt.verify(token, publicKey, { algorithms: ['ES256'] });` },
32
- // Multiple secure algorithms
33
- { code: `jwt.verify(token, key, { algorithms: ['RS256', 'RS384', 'RS512'] });` },
34
- // Single algorithm option
35
- { code: `jwt.verify(token, secret, { algorithm: 'HS256' });` },
36
- // jose library
37
- { code: `jwtVerify(token, key, { algorithms: ['RS256'] });` },
38
- // No options (doesn't trigger this rule)
39
- { code: `jwt.verify(token, secret);` },
40
- // Sign operation with secure algorithm
41
- { code: `jwt.sign(payload, secret, { algorithm: 'RS256' });` },
42
- // Spread operator in options (edge case - line 143 coverage)
43
- { code: `jwt.verify(token, key, { ...opts, algorithms: ['RS256'] });` },
44
- // Non-algorithm properties (line 152 coverage)
45
- { code: `jwt.verify(token, key, { issuer: 'auth', complete: true });` },
46
- // Options without algorithms (different property names)
47
- { code: `jwt.sign(payload, secret, { expiresIn: '1h', subject: 'user' });` },
48
- // Variable algorithms (not literal)
49
- { code: `jwt.verify(token, key, { algorithms: allowedAlgorithms });` },
50
- // Array with non-literal elements
51
- { code: `jwt.verify(token, key, { algorithms: [defaultAlg] });` },
52
- // Non-string literal in array
53
- { code: `jwt.verify(token, key, { algorithms: [123] });` },
54
- // Non-JWT function call - edge case (line 199 coverage)
55
- { code: `console.log('hello');` },
56
- { code: `someOtherFunction(token, key, { algorithm: 'none' });` },
57
- ],
58
- invalid: [],
59
- });
60
- });
61
-
62
- describe('Invalid Code - Algorithm None', () => {
63
- ruleTester.run('invalid - algorithm none', noAlgorithmNone, {
64
- valid: [],
65
- invalid: [
66
- // Direct 'none' algorithm
67
- {
68
- code: `jwt.verify(token, secret, { algorithm: 'none' });`,
69
- errors: [{ messageId: 'algorithmNone' }],
70
- },
71
- // 'none' in algorithms array (single element - gets replaced)
72
- {
73
- code: `jwt.verify(token, secret, { algorithms: ['none'] });`,
74
- errors: [{ messageId: 'algorithmNoneInArray' }],
75
- },
76
- // 'none' mixed with other algorithms (no auto-fix for removal)
77
- {
78
- code: `jwt.verify(token, secret, { algorithms: ['RS256', 'none'] });`,
79
- errors: [{ messageId: 'algorithmNoneInArray' }],
80
- },
81
- // Case insensitive - 'None'
82
- {
83
- code: `jwt.verify(token, secret, { algorithm: 'None' });`,
84
- errors: [{ messageId: 'algorithmNone' }],
85
- },
86
- // Case insensitive - 'NONE'
87
- {
88
- code: `jwt.verify(token, secret, { algorithms: ['NONE'] });`,
89
- errors: [{ messageId: 'algorithmNoneInArray' }],
90
- },
91
- // alg shorthand
92
- {
93
- code: `jwt.verify(token, secret, { alg: 'none' });`,
94
- errors: [{ messageId: 'algorithmNone' }],
95
- },
96
- // Empty algorithms array
97
- {
98
- code: `jwt.verify(token, secret, { algorithms: [] });`,
99
- errors: [{ messageId: 'emptyAlgorithms' }],
100
- },
101
- // Sign operation with none
102
- {
103
- code: `jwt.sign(payload, secret, { algorithm: 'none' });`,
104
- errors: [{ messageId: 'algorithmNone' }],
105
- },
106
- // jose library
107
- {
108
- code: `jwtVerify(token, key, { algorithms: ['none'] });`,
109
- errors: [{ messageId: 'algorithmNoneInArray' }],
110
- },
111
- // signJWT with none
112
- {
113
- code: `signJWT(payload, key, { algorithm: 'none' });`,
114
- errors: [{ messageId: 'algorithmNone' }],
115
- },
116
- ],
117
- });
118
- });
119
-
120
- describe('Configuration Options', () => {
121
- ruleTester.run('config - allowInTests', noAlgorithmNone, {
122
- valid: [
123
- // Test file with allowInTests enabled
124
- {
125
- code: `jwt.verify(token, secret, { algorithm: 'none' });`,
126
- options: [{ allowInTests: true }],
127
- filename: 'auth.test.ts',
128
- },
129
- {
130
- code: `jwt.verify(token, secret, { algorithm: 'none' });`,
131
- options: [{ allowInTests: true }],
132
- filename: '__tests__/jwt.ts',
133
- },
134
- ],
135
- invalid: [
136
- // Non-test file with allowInTests
137
- {
138
- code: `jwt.verify(token, secret, { algorithm: 'none' });`,
139
- options: [{ allowInTests: true }],
140
- filename: 'auth.ts',
141
- errors: [{ messageId: 'algorithmNone' }],
142
- },
143
- ],
144
- });
145
- });
146
- });
@@ -1,162 +0,0 @@
1
- /**
2
- * Copyright (c) 2025 Ofri Peretz
3
- * Licensed under the MIT License. Use of this source code is governed by the
4
- * MIT license that can be found in the LICENSE file.
5
- */
6
-
7
- /**
8
- * ESLint Rule: no-decode-without-verify
9
- *
10
- * Detects usage of jwt.decode() or jwt-decode library without corresponding
11
- * verification. Decoded JWTs can be tampered with by attackers.
12
- *
13
- * CWE-345: Insufficient Verification of Data Authenticity
14
- *
15
- * @see https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JSON_Web_Tokens
16
- */
17
- import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
18
- import { createRule, formatLLMMessage, MessageIcons, hasSafeAnnotation } from '@interlace/eslint-devkit';
19
- import { isDecodeOperation } from '../../utils';
20
- import type { NoDecodeWithoutVerifyOptions } from '../../types';
21
-
22
- type MessageIds =
23
- | 'decodeWithoutVerify'
24
- | 'jwtDecodeLibrary'
25
- | 'useVerifyInstead';
26
-
27
- type RuleOptions = [NoDecodeWithoutVerifyOptions?];
28
-
29
- export const noDecodeWithoutVerify = createRule<RuleOptions, MessageIds>({
30
- name: 'no-decode-without-verify',
31
- meta: {
32
- type: 'problem',
33
- docs: {
34
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-decode-without-verify.md',
35
- description:
36
- 'Disallow trusting decoded JWT payload without signature verification',
37
- cwe: 'CWE-345',
38
- cvss: 7.5,
39
- },
40
- fixable: undefined,
41
- hasSuggestions: false,
42
- messages: {
43
- decodeWithoutVerify: formatLLMMessage({
44
- icon: MessageIcons.SECURITY,
45
- issueName: 'Decoded JWT Without Verification',
46
- cwe: 'CWE-345',
47
- description:
48
- 'jwt.decode() returns payload without verifying signature - data can be forged',
49
- severity: 'HIGH',
50
- fix: 'Use jwt.verify(token, secret) instead of jwt.decode(token)',
51
- documentationLink:
52
- 'https://owasp.org/API-Security/0xa7-security-misconfiguration/',
53
- }),
54
- jwtDecodeLibrary: formatLLMMessage({
55
- icon: MessageIcons.SECURITY,
56
- issueName: 'jwt-decode Library Usage',
57
- cwe: 'CWE-345',
58
- description:
59
- 'jwt-decode library only decodes tokens, never verifies signatures',
60
- severity: 'HIGH',
61
- fix: 'Use jsonwebtoken.verify() or jose.jwtVerify() for verification',
62
- documentationLink: 'https://www.npmjs.com/package/jwt-decode',
63
- }),
64
- useVerifyInstead: formatLLMMessage({
65
- icon: MessageIcons.INFO,
66
- issueName: 'Use Verify Instead',
67
- description: 'Replace decode with verify to ensure authenticity',
68
- severity: 'LOW',
69
- fix: 'jwt.verify(token, secret, { algorithms: ["RS256"] })',
70
- documentationLink: 'https://www.npmjs.com/package/jsonwebtoken',
71
- }),
72
- },
73
- schema: [
74
- {
75
- type: 'object',
76
- properties: {
77
- allowHeaderInspection: {
78
- type: 'boolean',
79
- default: false,
80
- description:
81
- 'Allow decode() for reading header before verification',
82
- },
83
- trustedSanitizers: {
84
- type: 'array',
85
- items: { type: 'string' },
86
- default: [],
87
- },
88
- trustedAnnotations: {
89
- type: 'array',
90
- items: { type: 'string' },
91
- default: ['@decoded-header-only', '@verified-separately'],
92
- },
93
- strictMode: {
94
- type: 'boolean',
95
- default: false,
96
- },
97
- },
98
- additionalProperties: false,
99
- },
100
- ],
101
- },
102
- defaultOptions: [
103
- {
104
- allowHeaderInspection: false,
105
- trustedSanitizers: [],
106
- trustedAnnotations: ['@decoded-header-only', '@verified-separately'],
107
- strictMode: false,
108
- },
109
- ],
110
- create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
111
- const options = context.options[0] ?? {};
112
- const { trustedAnnotations = [] } = options;
113
-
114
- /**
115
- * Check if this is a jwt-decode import usage
116
- */
117
- // oxlint-disable-next-line consistent-function-scoping
118
- const isJwtDecodeLibrary = (node: TSESTree.CallExpression): boolean => {
119
- // Direct call: jwtDecode(token)
120
- if (
121
- node.callee.type === 'Identifier' &&
122
- (node.callee.name === 'jwtDecode' || node.callee.name === 'jwt_decode')
123
- ) {
124
- return true;
125
- }
126
- return false;
127
- };
128
-
129
- return {
130
- CallExpression(node: TSESTree.CallExpression) {
131
- // Check for jwt.decode() pattern
132
- if (isDecodeOperation(node)) {
133
- // Check for safe annotations
134
- if (hasSafeAnnotation(node, context, trustedAnnotations)) {
135
- return;
136
- }
137
-
138
- context.report({
139
- node,
140
- messageId: 'decodeWithoutVerify',
141
- });
142
- return;
143
- }
144
-
145
- // Check for jwt-decode library usage
146
- if (isJwtDecodeLibrary(node)) {
147
- // Check for safe annotations
148
- if (hasSafeAnnotation(node, context, trustedAnnotations)) {
149
- return;
150
- }
151
-
152
- context.report({
153
- node,
154
- messageId: 'jwtDecodeLibrary',
155
- });
156
- }
157
- },
158
- };
159
- },
160
- });
161
-
162
- export default noDecodeWithoutVerify;
@@ -1,76 +0,0 @@
1
- /**
2
- * Tests for no-decode-without-verify rule
3
- * Security: CWE-345 (Data Authenticity)
4
- */
5
- import { RuleTester } from '@typescript-eslint/rule-tester';
6
- import { describe, it, afterAll } from 'vitest';
7
- import parser from '@typescript-eslint/parser';
8
- import { noDecodeWithoutVerify } from './index';
9
-
10
- RuleTester.afterAll = afterAll;
11
- RuleTester.it = it;
12
- RuleTester.itOnly = it.only;
13
- RuleTester.describe = describe;
14
-
15
- const ruleTester = new RuleTester({
16
- languageOptions: {
17
- parser,
18
- ecmaVersion: 2022,
19
- sourceType: 'module',
20
- },
21
- });
22
-
23
- describe('no-decode-without-verify', () => {
24
- describe('Valid Code - Verify Operations', () => {
25
- ruleTester.run('valid - verify operations', noDecodeWithoutVerify, {
26
- valid: [
27
- // jwt.verify is safe
28
- { code: `const payload = jwt.verify(token, secret);` },
29
- { code: `jwt.verify(token, publicKey, { algorithms: ['RS256'] });` },
30
- // jose jwtVerify is safe
31
- { code: `const { payload } = await jwtVerify(token, key);` },
32
- // sign is not flagged
33
- { code: `jwt.sign(payload, secret);` },
34
- ],
35
- invalid: [],
36
- });
37
- });
38
-
39
- describe('Invalid Code - Decode Operations', () => {
40
- ruleTester.run('invalid - decode without verify', noDecodeWithoutVerify, {
41
- valid: [],
42
- invalid: [
43
- // Basic jwt.decode()
44
- {
45
- code: `const payload = jwt.decode(token);`,
46
- errors: [{ messageId: 'decodeWithoutVerify' }],
47
- },
48
- // decode with options
49
- {
50
- code: `const decoded = jwt.decode(token, { complete: true });`,
51
- errors: [{ messageId: 'decodeWithoutVerify' }],
52
- },
53
- // Using payload directly
54
- {
55
- code: `const userId = jwt.decode(token).sub;`,
56
- errors: [{ messageId: 'decodeWithoutVerify' }],
57
- },
58
- // jwt-decode library - jwtDecode pattern
59
- {
60
- code: `const payload = jwtDecode(accessToken);`,
61
- errors: [{ messageId: 'decodeWithoutVerify' }],
62
- },
63
- // jwt-decode library - jwt_decode pattern (snake_case import)
64
- {
65
- code: `const payload = jwt_decode(accessToken);`,
66
- errors: [{ messageId: 'jwtDecodeLibrary' }],
67
- },
68
- // jose decodeJwt (decode only)
69
- {
70
- code: `const payload = decodeJWT(token);`,
71
- errors: [{ messageId: 'decodeWithoutVerify' }],
72
- },
73
- ],
74
- });
75
- });
76
- });
@@ -1,230 +0,0 @@
1
- /**
2
- * Copyright (c) 2025 Ofri Peretz
3
- * Licensed under the MIT License. Use of this source code is governed by the
4
- * MIT license that can be found in the LICENSE file.
5
- */
6
-
7
- /**
8
- * ESLint Rule: no-hardcoded-secret
9
- *
10
- * Detects hardcoded secrets in JWT sign/verify operations.
11
- * Hardcoded secrets in source code are a critical security vulnerability.
12
- *
13
- * CWE-798: Use of Hard-coded Credentials
14
- *
15
- * @see https://cwe.mitre.org/data/definitions/798.html
16
- */
17
- import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
18
- import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
19
- import {
20
- isSignOperation,
21
- isVerifyOperation,
22
- isEnvVariable,
23
- } from '../../utils';
24
- import type { NoHardcodedSecretOptions } from '../../types';
25
-
26
- type MessageIds =
27
- | 'hardcodedSecret'
28
- | 'useEnvVariable';
29
-
30
- type RuleOptions = [NoHardcodedSecretOptions?];
31
-
32
- export const noHardcodedSecret = createRule<RuleOptions, MessageIds>({
33
- name: 'no-hardcoded-secret',
34
- meta: {
35
- type: 'problem',
36
- docs: {
37
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-hardcoded-secret.md',
38
- description:
39
- 'Disallow hardcoded secrets in JWT sign/verify operations',
40
- cwe: 'CWE-798',
41
- cvss: 9.8,
42
- },
43
- fixable: undefined,
44
- hasSuggestions: false,
45
- messages: {
46
- hardcodedSecret: formatLLMMessage({
47
- icon: MessageIcons.SECURITY,
48
- issueName: 'Hardcoded JWT Secret',
49
- cwe: 'CWE-798',
50
- description:
51
- 'Secret key hardcoded in source code can be extracted from repositories',
52
- severity: 'HIGH',
53
- fix: 'Use process.env.JWT_SECRET or secure secret management',
54
- documentationLink: 'https://cwe.mitre.org/data/definitions/798.html',
55
- }),
56
- useEnvVariable: formatLLMMessage({
57
- icon: MessageIcons.INFO,
58
- issueName: 'Use Environment Variable',
59
- description: 'Replace hardcoded secret with environment variable',
60
- severity: 'LOW',
61
- fix: 'process.env.JWT_SECRET',
62
- documentationLink:
63
- 'https://12factor.net/config',
64
- }),
65
- },
66
- schema: [
67
- {
68
- type: 'object',
69
- properties: {
70
- envPatterns: {
71
- type: 'array',
72
- items: { type: 'string' },
73
- default: [],
74
- description: 'Patterns that indicate safe environment variable usage',
75
- },
76
- trustedSanitizers: {
77
- type: 'array',
78
- items: { type: 'string' },
79
- default: [],
80
- },
81
- trustedAnnotations: {
82
- type: 'array',
83
- items: { type: 'string' },
84
- default: [],
85
- },
86
- strictMode: {
87
- type: 'boolean',
88
- default: false,
89
- },
90
- },
91
- additionalProperties: false,
92
- },
93
- ],
94
- },
95
- defaultOptions: [
96
- {
97
- envPatterns: [],
98
- trustedSanitizers: [],
99
- trustedAnnotations: [],
100
- strictMode: false,
101
- },
102
- ],
103
- create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
104
- /**
105
- * Check if a node is a hardcoded string literal
106
- */
107
- // oxlint-disable-next-line consistent-function-scoping
108
- const isHardcodedString = (node: TSESTree.Node): boolean => {
109
- // String literal
110
- if (node.type === 'Literal' && typeof node.value === 'string') {
111
- return true;
112
- }
113
-
114
- // Template literal without expressions
115
- if (
116
- node.type === 'TemplateLiteral' &&
117
- node.expressions.length === 0
118
- ) {
119
- return true;
120
- }
121
-
122
- return false;
123
- };
124
-
125
- /**
126
- * Resolve an Identifier node to its initializer (one frame of indirection).
127
- * Closes the audit FN where `const SECRET = 'x'; jwt.sign(p, SECRET)`
128
- * was treated as safe — the literal-via-const pattern hides the
129
- * hardcoded secret behind one declaration. See benchmarks/AUDIT_PATTERNS.md
130
- * §3.2 ("Indirection through one level of variable").
131
- */
132
- const resolveConstLiteralValue = (
133
- node: TSESTree.Node,
134
- ): TSESTree.Node | null => {
135
- if (node.type !== 'Identifier') return null;
136
- const scope = context.sourceCode.getScope?.(node) ?? null;
137
- if (!scope) return null;
138
- const variable = scope.references.find(
139
- (r) => r.identifier === node,
140
- )?.resolved;
141
- const def = variable?.defs[0];
142
- if (!def || def.type !== 'Variable') return null;
143
- const decl = def.parent;
144
- if (decl?.type !== 'VariableDeclaration' || decl.kind !== 'const') return null;
145
- return def.node.init ?? null;
146
- };
147
-
148
- /**
149
- * Check if node is a safe key source
150
- */
151
- const isSafeKeySource = (node: TSESTree.Node): boolean => {
152
- // Environment variable
153
- if (isEnvVariable(node)) {
154
- return true;
155
- }
156
-
157
- // Function call (getSecret(), loadKey(), etc.)
158
- if (node.type === 'CallExpression') {
159
- return true;
160
- }
161
-
162
- // await expression (async key loading)
163
- if (node.type === 'AwaitExpression') {
164
- return true;
165
- }
166
-
167
- // Variable reference: resolve `const X = ...` one level. If X is a
168
- // hardcoded literal, treat as unsafe — fall through to the
169
- // hardcoded check. Otherwise (env var, call, identifier chain)
170
- // it's safe.
171
- if (node.type === 'Identifier') {
172
- const init = resolveConstLiteralValue(node);
173
- if (init && isHardcodedString(init)) {
174
- // Unsafe — let the caller's `isHardcodedString` (with the
175
- // resolved init) flag it.
176
- return false;
177
- }
178
- return true;
179
- }
180
-
181
- // Member expression but not literal (config.secret, etc.)
182
- if (node.type === 'MemberExpression' && !isHardcodedString(node)) {
183
- return true;
184
- }
185
-
186
- return false;
187
- };
188
-
189
- /**
190
- * Like isHardcodedString but resolves single-level `const X = '...'`
191
- * indirection so the caller fires on `const SECRET = 'x'; jwt.sign(p, SECRET)`.
192
- */
193
- const isHardcodedStringOrResolvedConst = (node: TSESTree.Node): boolean => {
194
- if (isHardcodedString(node)) return true;
195
- const init = resolveConstLiteralValue(node);
196
- return init ? isHardcodedString(init) : false;
197
- };
198
-
199
- return {
200
- CallExpression(node: TSESTree.CallExpression) {
201
- // Check both sign and verify operations
202
- if (!isSignOperation(node) && !isVerifyOperation(node)) {
203
- return;
204
- }
205
-
206
- // Secret/key is the second argument
207
- if (node.arguments.length < 2) {
208
- return;
209
- }
210
-
211
- const secretArg = node.arguments[1];
212
-
213
- // Skip if safe source
214
- if (isSafeKeySource(secretArg)) {
215
- return;
216
- }
217
-
218
- // Flag hardcoded strings (also follows single-frame `const X = '...'`).
219
- if (isHardcodedStringOrResolvedConst(secretArg)) {
220
- context.report({
221
- node: secretArg,
222
- messageId: 'hardcodedSecret',
223
- });
224
- }
225
- },
226
- };
227
- },
228
- });
229
-
230
- export default noHardcodedSecret;