eslint-plugin-node-security 4.7.3 → 4.8.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.
Files changed (41) hide show
  1. package/package.json +2 -2
  2. package/src/index.js +1 -95
  3. package/src/oxlint.js +1 -3
  4. package/src/rules/detect-child-process/index.js +1 -625
  5. package/src/rules/detect-eval-with-expression/index.js +1 -364
  6. package/src/rules/detect-non-literal-fs-filename/index.js +1 -431
  7. package/src/rules/detect-suspicious-dependencies/index.js +1 -69
  8. package/src/rules/lock-file/index.js +1 -92
  9. package/src/rules/no-arbitrary-file-access/index.js +1 -152
  10. package/src/rules/no-buffer-overread/index.js +1 -543
  11. package/src/rules/no-cryptojs/index.js +1 -99
  12. package/src/rules/no-cryptojs-weak-random/index.js +1 -103
  13. package/src/rules/no-data-in-temp-storage/index.js +1 -85
  14. package/src/rules/no-deprecated-buffer/index.js +1 -84
  15. package/src/rules/no-deprecated-cipher-method/index.js +1 -112
  16. package/src/rules/no-dynamic-algorithm-selection/index.js +1 -72
  17. package/src/rules/no-dynamic-command-string/index.js +1 -201
  18. package/src/rules/no-dynamic-dependency-loading/index.js +1 -45
  19. package/src/rules/no-dynamic-require/index.js +1 -96
  20. package/src/rules/no-ecb-mode/index.js +1 -108
  21. package/src/rules/no-insecure-key-derivation/index.js +1 -109
  22. package/src/rules/no-insecure-rsa-padding/index.js +1 -104
  23. package/src/rules/no-math-random-crypto/index.js +1 -192
  24. package/src/rules/no-self-signed-certs/index.js +1 -110
  25. package/src/rules/no-sha1-hash/index.js +1 -121
  26. package/src/rules/no-shell-injection/index.js +1 -68
  27. package/src/rules/no-ssrf/index.js +1 -221
  28. package/src/rules/no-static-iv/index.js +1 -129
  29. package/src/rules/no-timing-unsafe-compare/index.js +1 -106
  30. package/src/rules/no-toctou-vulnerability/index.js +1 -195
  31. package/src/rules/no-unsafe-buffer-alloc/index.js +1 -87
  32. package/src/rules/no-unsafe-dynamic-require/index.js +1 -93
  33. package/src/rules/no-weak-cipher-algorithm/index.js +1 -174
  34. package/src/rules/no-weak-hash-algorithm/index.js +1 -199
  35. package/src/rules/no-zip-slip/index.js +1 -410
  36. package/src/rules/prefer-native-crypto/index.js +1 -119
  37. package/src/rules/require-dependency-integrity/index.js +1 -62
  38. package/src/rules/require-secure-credential-storage/index.js +1 -45
  39. package/src/rules/require-secure-deletion/index.js +1 -82
  40. package/src/rules/require-storage-encryption/index.js +1 -45
  41. package/src/types/index.js +1 -2
@@ -1,199 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noWeakHashAlgorithm = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- const WEAK_HASH_PATTERNS = [
6
- {
7
- pattern: /\bmd5\b/i,
8
- name: 'MD5',
9
- alternatives: ['SHA-256', 'SHA-512', 'SHA-3'],
10
- replacement: 'sha256',
11
- },
12
- {
13
- pattern: /\bmd4\b/i,
14
- name: 'MD4',
15
- alternatives: ['SHA-256', 'SHA-512', 'SHA-3'],
16
- replacement: 'sha256',
17
- },
18
- {
19
- pattern: /\bsha1\b/i,
20
- name: 'SHA-1',
21
- alternatives: ['SHA-256', 'SHA-512', 'SHA-3'],
22
- replacement: 'sha256',
23
- },
24
- {
25
- pattern: /\bripemd\b/i,
26
- name: 'RIPEMD',
27
- alternatives: ['SHA-256', 'SHA-512'],
28
- replacement: 'sha256',
29
- },
30
- ];
31
- function findWeakHash(value, additionalPatterns) {
32
- for (const pattern of WEAK_HASH_PATTERNS) {
33
- if (pattern.pattern.test(value)) {
34
- return pattern;
35
- }
36
- }
37
- for (const additionalPattern of additionalPatterns) {
38
- const regex = new RegExp(`\\b${additionalPattern}\\b`, 'i');
39
- if (regex.test(value)) {
40
- return {
41
- pattern: regex,
42
- name: additionalPattern.toUpperCase(),
43
- alternatives: ['SHA-256', 'SHA-512'],
44
- replacement: 'sha256',
45
- };
46
- }
47
- }
48
- return null;
49
- }
50
- exports.noWeakHashAlgorithm = (0, eslint_devkit_1.createRule)({
51
- name: 'no-weak-hash-algorithm',
52
- meta: {
53
- type: 'problem',
54
- docs: {
55
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-weak-hash-algorithm.md',
56
- description: 'Disallow weak hash algorithms (MD5, SHA1, MD4)',
57
- cwe: 'CWE-327',
58
- cvss: 7.5,
59
- },
60
- hasSuggestions: true,
61
- messages: {
62
- weakHashAlgorithm: (0, eslint_devkit_1.formatLLMMessage)({
63
- icon: eslint_devkit_1.MessageIcons.SECURITY,
64
- issueName: 'Weak hash algorithm',
65
- cwe: 'CWE-327',
66
- description: 'Use of weak hash algorithm: {{algorithm}}. {{algorithm}} is cryptographically broken and unsuitable for security purposes.',
67
- severity: 'CRITICAL',
68
- fix: 'Replace with {{replacement}}: crypto.createHash("{{replacement}}").update(data)',
69
- documentationLink: 'https://owasp.org/www-community/vulnerabilities/Weak_Cryptography',
70
- }),
71
- useSha256: (0, eslint_devkit_1.formatLLMMessage)({
72
- icon: eslint_devkit_1.MessageIcons.INFO,
73
- issueName: 'Use SHA-256',
74
- description: 'Replace with SHA-256 for secure hashing',
75
- severity: 'LOW',
76
- fix: 'crypto.createHash("sha256").update(data)',
77
- documentationLink: 'https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options',
78
- }),
79
- useSha512: (0, eslint_devkit_1.formatLLMMessage)({
80
- icon: eslint_devkit_1.MessageIcons.INFO,
81
- issueName: 'Use SHA-512',
82
- description: 'Replace with SHA-512 for stronger hashing',
83
- severity: 'LOW',
84
- fix: 'crypto.createHash("sha512").update(data)',
85
- documentationLink: 'https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options',
86
- }),
87
- useSha3: (0, eslint_devkit_1.formatLLMMessage)({
88
- icon: eslint_devkit_1.MessageIcons.INFO,
89
- issueName: 'Use SHA-3',
90
- description: 'Replace with SHA-3 for latest standard',
91
- severity: 'LOW',
92
- fix: 'crypto.createHash("sha3-256").update(data)',
93
- documentationLink: 'https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options',
94
- }),
95
- },
96
- schema: [
97
- {
98
- type: 'object',
99
- properties: {
100
- additionalWeakAlgorithms: {
101
- type: 'array',
102
- items: { type: 'string' },
103
- default: [],
104
- description: 'Additional weak algorithms to detect',
105
- },
106
- allowInTests: {
107
- type: 'boolean',
108
- default: false,
109
- description: 'Allow weak hashes in test files',
110
- },
111
- },
112
- additionalProperties: false,
113
- },
114
- ],
115
- },
116
- defaultOptions: [
117
- {
118
- additionalWeakAlgorithms: [],
119
- allowInTests: false,
120
- },
121
- ],
122
- create(context, [options = {}]) {
123
- const { additionalWeakAlgorithms = [], allowInTests = false, } = options;
124
- const filename = context.filename;
125
- const isTestFile = allowInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
126
- function checkCallExpression(node) {
127
- if (isTestFile)
128
- return;
129
- if (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
130
- node.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
131
- node.callee.property.name === 'createHash') {
132
- checkHashArgument(node);
133
- }
134
- if (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
135
- node.callee.name === 'createHash') {
136
- checkHashArgument(node);
137
- }
138
- if (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier) {
139
- const funcName = node.callee.name.toLowerCase();
140
- if (funcName === 'sha1' || funcName === 'md5' || funcName === 'md4') {
141
- const weakPattern = findWeakHash(funcName, additionalWeakAlgorithms);
142
- context.report({
143
- node,
144
- messageId: 'weakHashAlgorithm',
145
- data: {
146
- algorithm: weakPattern.name,
147
- replacement: weakPattern.replacement,
148
- },
149
- suggest: [
150
- {
151
- messageId: 'useSha256',
152
- fix: (fixer) => {
153
- if (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier) {
154
- return fixer.replaceText(node.callee, 'sha256');
155
- }
156
- return null;
157
- },
158
- },
159
- ],
160
- });
161
- }
162
- }
163
- }
164
- function checkHashArgument(node) {
165
- for (const arg of node.arguments) {
166
- if (arg.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof arg.value === 'string') {
167
- const weakPattern = findWeakHash(arg.value, additionalWeakAlgorithms);
168
- if (weakPattern) {
169
- context.report({
170
- node: arg,
171
- messageId: 'weakHashAlgorithm',
172
- data: {
173
- algorithm: weakPattern.name,
174
- replacement: weakPattern.replacement,
175
- },
176
- suggest: [
177
- {
178
- messageId: 'useSha256',
179
- fix: (fixer) => fixer.replaceText(arg, `"sha256"`),
180
- },
181
- {
182
- messageId: 'useSha512',
183
- fix: (fixer) => fixer.replaceText(arg, `"sha512"`),
184
- },
185
- {
186
- messageId: 'useSha3',
187
- fix: (fixer) => fixer.replaceText(arg, `"sha3-256"`),
188
- },
189
- ],
190
- });
191
- }
192
- }
193
- }
194
- }
195
- return {
196
- CallExpression: checkCallExpression,
197
- };
198
- },
199
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noWeakHashAlgorithm=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const WEAK_HASH_PATTERNS=[{pattern:/\bmd5\b/i,name:"MD5",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bmd4\b/i,name:"MD4",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bsha1\b/i,name:"SHA-1",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bripemd\b/i,name:"RIPEMD",alternatives:["SHA-256","SHA-512"],replacement:"sha256"}];function findWeakHash(value,additionalPatterns){for(const pattern of WEAK_HASH_PATTERNS){if(pattern.pattern.test(value)){return pattern}}for(const additionalPattern of additionalPatterns){const regex=new RegExp(`\\b${additionalPattern}\\b`,"i");if(regex.test(value)){return{pattern:regex,name:additionalPattern.toUpperCase(),alternatives:["SHA-256","SHA-512"],replacement:"sha256"}}}return null}exports.noWeakHashAlgorithm=(0,eslint_devkit_1.createRule)({name:"no-weak-hash-algorithm",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-weak-hash-algorithm.md",description:"Disallow weak hash algorithms (MD5, SHA1, MD4)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{weakHashAlgorithm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Weak hash algorithm",cwe:"CWE-327",description:"Use of weak hash algorithm: {{algorithm}}. {{algorithm}} is cryptographically broken and unsuitable for security purposes.",severity:"CRITICAL",fix:'Replace with {{replacement}}: crypto.createHash("{{replacement}}").update(data)',documentationLink:"https://owasp.org/www-community/vulnerabilities/Weak_Cryptography"}),useSha256:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-256",description:"Replace with SHA-256 for secure hashing",severity:"LOW",fix:'crypto.createHash("sha256").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"}),useSha512:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-512",description:"Replace with SHA-512 for stronger hashing",severity:"LOW",fix:'crypto.createHash("sha512").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"}),useSha3:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-3",description:"Replace with SHA-3 for latest standard",severity:"LOW",fix:'crypto.createHash("sha3-256").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"})},schema:[{type:"object",properties:{additionalWeakAlgorithms:{type:"array",items:{type:"string"},default:[],description:"Additional weak algorithms to detect"},allowInTests:{type:"boolean",default:false,description:"Allow weak hashes in test files"}},additionalProperties:false}]},defaultOptions:[{additionalWeakAlgorithms:[],allowInTests:false}],create(context,[options={}]){const{additionalWeakAlgorithms=[],allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);function checkCallExpression(node){if(isTestFile)return;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.name==="createHash"){checkHashArgument(node)}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="createHash"){checkHashArgument(node)}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const funcName=node.callee.name.toLowerCase();if(funcName==="sha1"||funcName==="md5"||funcName==="md4"){const weakPattern=findWeakHash(funcName,additionalWeakAlgorithms);context.report({node,messageId:"weakHashAlgorithm",data:{algorithm:weakPattern.name,replacement:weakPattern.replacement},suggest:[{messageId:"useSha256",fix:fixer=>{if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return fixer.replaceText(node.callee,"sha256")}return null}}]})}}}function checkHashArgument(node){for(const arg of node.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof arg.value==="string"){const weakPattern=findWeakHash(arg.value,additionalWeakAlgorithms);if(weakPattern){context.report({node:arg,messageId:"weakHashAlgorithm",data:{algorithm:weakPattern.name,replacement:weakPattern.replacement},suggest:[{messageId:"useSha256",fix:fixer=>fixer.replaceText(arg,`"sha256"`)},{messageId:"useSha512",fix:fixer=>fixer.replaceText(arg,`"sha512"`)},{messageId:"useSha3",fix:fixer=>fixer.replaceText(arg,`"sha3-256"`)}]})}}}}return{CallExpression:checkCallExpression}}});
@@ -1,410 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noZipSlip = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- const eslint_devkit_2 = require("@interlace/eslint-devkit");
6
- const containsPathTraversal = (pathText) => {
7
- return /\.\.\//.test(pathText) ||
8
- /\.\.\\/.test(pathText) ||
9
- pathText.startsWith('..') ||
10
- /\/\.\./.test(pathText);
11
- };
12
- const isDangerousDestination = (destText) => {
13
- if (destText.startsWith('/tmp') || destText.includes('os.tmpdir') || destText.includes('TMPDIR')) {
14
- return false;
15
- }
16
- return destText.includes('/var') ||
17
- destText.includes('/usr') ||
18
- destText.includes('/etc') ||
19
- destText.includes('/root') ||
20
- destText.includes('/home') ||
21
- destText.includes('C:\\Windows') ||
22
- destText.includes('C:\\Program Files') ||
23
- destText.includes('C:\\Users');
24
- };
25
- exports.noZipSlip = (0, eslint_devkit_1.createRule)({
26
- name: 'no-zip-slip',
27
- meta: {
28
- type: 'problem',
29
- docs: {
30
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-zip-slip.md',
31
- description: 'Detects zip slip/archive extraction vulnerabilities',
32
- cwe: 'CWE-22',
33
- },
34
- hasSuggestions: true,
35
- messages: {
36
- zipSlipVulnerability: (0, eslint_devkit_2.formatLLMMessage)({
37
- icon: eslint_devkit_2.MessageIcons.SECURITY,
38
- issueName: 'Zip Slip Vulnerability',
39
- cwe: 'CWE-22',
40
- description: 'Archive extraction vulnerable to path traversal',
41
- severity: '{{severity}}',
42
- fix: '{{safeAlternative}}',
43
- documentationLink: 'https://cwe.mitre.org/data/definitions/22.html',
44
- }),
45
- unsafeArchiveExtraction: (0, eslint_devkit_2.formatLLMMessage)({
46
- icon: eslint_devkit_2.MessageIcons.SECURITY,
47
- issueName: 'Unsafe Archive Extraction',
48
- cwe: 'CWE-22',
49
- description: 'Archive extraction without path validation',
50
- severity: 'HIGH',
51
- fix: 'Use safe extraction libraries or validate all paths',
52
- documentationLink: 'https://snyk.io/research/zip-slip-vulnerability',
53
- }),
54
- pathTraversalInArchive: (0, eslint_devkit_2.formatLLMMessage)({
55
- icon: eslint_devkit_2.MessageIcons.SECURITY,
56
- issueName: 'Path Traversal in Archive',
57
- cwe: 'CWE-22',
58
- description: 'Archive contains path traversal sequences',
59
- severity: 'CRITICAL',
60
- fix: 'Reject archives with path traversal or sanitize paths',
61
- documentationLink: 'https://cwe.mitre.org/data/definitions/22.html',
62
- }),
63
- unvalidatedArchivePath: (0, eslint_devkit_2.formatLLMMessage)({
64
- icon: eslint_devkit_2.MessageIcons.SECURITY,
65
- issueName: 'Unvalidated Archive Path',
66
- cwe: 'CWE-22',
67
- description: 'Archive entry path used without validation',
68
- severity: 'HIGH',
69
- fix: 'Validate paths before extraction',
70
- documentationLink: 'https://snyk.io/research/zip-slip-vulnerability',
71
- }),
72
- dangerousArchiveDestination: (0, eslint_devkit_2.formatLLMMessage)({
73
- icon: eslint_devkit_2.MessageIcons.SECURITY,
74
- issueName: 'Dangerous Archive Destination',
75
- cwe: 'CWE-22',
76
- description: 'Archive extracted to sensitive location',
77
- severity: 'MEDIUM',
78
- fix: 'Extract to safe temporary directory',
79
- documentationLink: 'https://cwe.mitre.org/data/definitions/22.html',
80
- }),
81
- useSafeArchiveExtraction: (0, eslint_devkit_2.formatLLMMessage)({
82
- icon: eslint_devkit_2.MessageIcons.INFO,
83
- issueName: 'Use Safe Archive Extraction',
84
- description: 'Use libraries with built-in path validation',
85
- severity: 'LOW',
86
- fix: 'Use yauzl, safe-archive-extract, or similar safe libraries',
87
- documentationLink: 'https://www.npmjs.com/package/yauzl',
88
- }),
89
- validateArchivePaths: (0, eslint_devkit_2.formatLLMMessage)({
90
- icon: eslint_devkit_2.MessageIcons.INFO,
91
- issueName: 'Validate Archive Paths',
92
- description: 'Validate all archive entry paths',
93
- severity: 'LOW',
94
- fix: 'Check paths don\'t contain ../ and are within destination directory',
95
- documentationLink: 'https://snyk.io/research/zip-slip-vulnerability',
96
- }),
97
- sanitizeArchiveNames: (0, eslint_devkit_2.formatLLMMessage)({
98
- icon: eslint_devkit_2.MessageIcons.INFO,
99
- issueName: 'Sanitize Archive Names',
100
- description: 'Sanitize archive entry names',
101
- severity: 'LOW',
102
- fix: 'Use path.basename() or custom sanitization',
103
- documentationLink: 'https://nodejs.org/api/path.html#pathbasenamepath-ext',
104
- }),
105
- strategyPathValidation: (0, eslint_devkit_2.formatLLMMessage)({
106
- icon: eslint_devkit_2.MessageIcons.STRATEGY,
107
- issueName: 'Path Validation Strategy',
108
- description: 'Validate paths before any file operations',
109
- severity: 'LOW',
110
- fix: 'Check path.startsWith(destination) and no ../ sequences',
111
- documentationLink: 'https://cwe.mitre.org/data/definitions/22.html',
112
- }),
113
- strategySafeLibraries: (0, eslint_devkit_2.formatLLMMessage)({
114
- icon: eslint_devkit_2.MessageIcons.STRATEGY,
115
- issueName: 'Safe Libraries Strategy',
116
- description: 'Use archive libraries with built-in safety',
117
- severity: 'LOW',
118
- fix: 'Use yauzl, adm-zip with validation, or safe-archive-extract',
119
- documentationLink: 'https://www.npmjs.com/package/safe-archive-extract',
120
- }),
121
- strategySandboxing: (0, eslint_devkit_2.formatLLMMessage)({
122
- icon: eslint_devkit_2.MessageIcons.STRATEGY,
123
- issueName: 'Sandboxing Strategy',
124
- description: 'Extract archives in sandboxed environment',
125
- severity: 'LOW',
126
- fix: 'Use temporary directories and restrict permissions',
127
- documentationLink: 'https://nodejs.org/api/fs.html#fsopentempdirprefix-options-callback',
128
- })
129
- },
130
- schema: [
131
- {
132
- type: 'object',
133
- properties: {
134
- archiveFunctions: {
135
- type: 'array',
136
- items: { type: 'string' },
137
- default: ['extract', 'extractAll', 'extractAllTo', 'unzip', 'untar', 'extractArchive'],
138
- },
139
- pathValidationFunctions: {
140
- type: 'array',
141
- items: { type: 'string' },
142
- default: ['validatePath', 'sanitizePath', 'checkPath', 'safePath'],
143
- },
144
- safeLibraries: {
145
- type: 'array',
146
- items: { type: 'string' },
147
- default: ['yauzl', 'safe-archive-extract', 'tar-stream', 'unzipper'],
148
- },
149
- },
150
- additionalProperties: false,
151
- },
152
- ],
153
- },
154
- defaultOptions: [
155
- {
156
- archiveFunctions: ['extract', 'extractAll', 'extractAllTo', 'unzip', 'untar', 'extractArchive'],
157
- pathValidationFunctions: ['validatePath', 'sanitizePath', 'checkPath', 'safePath'],
158
- safeLibraries: ['yauzl', 'safe-archive-extract', 'tar-stream', 'unzipper'],
159
- },
160
- ],
161
- create(context) {
162
- const options = context.options[0] || {};
163
- const { archiveFunctions = ['extract', 'extractAll', 'extractAllTo', 'unzip', 'untar', 'extractArchive'], pathValidationFunctions = ['validatePath', 'sanitizePath', 'checkPath', 'safePath'], safeLibraries = ['yauzl', 'safe-archive-extract', 'tar-stream', 'unzipper'], } = options;
164
- const filename = context.filename;
165
- const isArchiveExtraction = (node) => {
166
- const callee = node.callee;
167
- if (callee.type === 'MemberExpression' &&
168
- callee.property.type === 'Identifier' &&
169
- archiveFunctions.includes(callee.property.name)) {
170
- return true;
171
- }
172
- if (callee.type === 'Identifier' &&
173
- archiveFunctions.includes(callee.name)) {
174
- return true;
175
- }
176
- return false;
177
- };
178
- const isPathValidated = (pathNode) => {
179
- let current = pathNode;
180
- while (current) {
181
- if (current.type === 'CallExpression' &&
182
- current.callee.type === 'Identifier' &&
183
- pathValidationFunctions.includes(current.callee.name)) {
184
- return true;
185
- }
186
- if (current.type === 'CallExpression' &&
187
- current.callee.type === 'MemberExpression' &&
188
- current.callee.object.type === 'Identifier' &&
189
- current.callee.object.name === 'path' &&
190
- current.callee.property.type === 'Identifier' &&
191
- current.callee.property.name === 'basename') {
192
- return true;
193
- }
194
- if (current.type === 'IfStatement') {
195
- const test = current.test;
196
- if (test.type === 'CallExpression' &&
197
- test.callee.type === 'MemberExpression' &&
198
- test.callee.property.type === 'Identifier' &&
199
- test.callee.property.name === 'startsWith') {
200
- return true;
201
- }
202
- if (test.type === 'UnaryExpression' && test.operator === '!' &&
203
- test.argument.type === 'CallExpression' &&
204
- test.argument.callee.type === 'MemberExpression' &&
205
- test.argument.callee.property.type === 'Identifier' &&
206
- test.argument.callee.property.name === 'startsWith') {
207
- return true;
208
- }
209
- if (test.type === 'CallExpression' &&
210
- test.callee.type === 'MemberExpression' &&
211
- test.callee.property.type === 'Identifier' &&
212
- test.callee.property.name === 'includes') {
213
- return true;
214
- }
215
- }
216
- current = current.parent;
217
- }
218
- return false;
219
- };
220
- const isSafeLibrary = (node) => {
221
- const callee = node.callee;
222
- if (callee.type === 'MemberExpression' &&
223
- callee.object.type === 'Identifier' &&
224
- safeLibraries.includes(callee.object.name)) {
225
- return true;
226
- }
227
- if (callee.type === 'Identifier') {
228
- const name = callee.name.toLowerCase();
229
- if (name === 'extract' || name === 'unzipper' ||
230
- safeLibraries.some(lib => name.includes(lib.toLowerCase()))) {
231
- return true;
232
- }
233
- }
234
- return false;
235
- };
236
- return {
237
- CallExpression(node) {
238
- if (isArchiveExtraction(node) && !isSafeLibrary(node)) {
239
- const sourceCode = context.sourceCode;
240
- let hasSafeAnnotation = false;
241
- const allComments = sourceCode.getAllComments();
242
- for (const comment of allComments) {
243
- if (comment.type === 'Block' && comment.value.includes('@safe')) {
244
- hasSafeAnnotation = true;
245
- break;
246
- }
247
- }
248
- if (hasSafeAnnotation) {
249
- return;
250
- }
251
- const args = node.arguments;
252
- let destArg;
253
- if (node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier') {
254
- const methodName = node.callee.property.name;
255
- if (['extractAllTo', 'unzip'].includes(methodName)) {
256
- destArg = args[0];
257
- }
258
- else {
259
- destArg = args.length >= 2 ? args[1] : undefined;
260
- }
261
- }
262
- else {
263
- destArg = args.length >= 2 ? args[1] : undefined;
264
- }
265
- const destText = destArg && destArg.type === 'Literal' && typeof destArg.value === 'string' ? destArg.value : '';
266
- const isDestDangerous = isDangerousDestination(destText);
267
- const isMethodCall = node.callee.type === 'MemberExpression';
268
- if (isMethodCall) {
269
- const isSafeRelativePath = destText.startsWith('./') || destText.startsWith('../');
270
- if (!isSafeRelativePath) {
271
- context.report({
272
- node,
273
- messageId: 'unsafeArchiveExtraction',
274
- data: {
275
- filePath: filename,
276
- line: String(node.loc?.start.line ?? 0),
277
- },
278
- suggest: [
279
- {
280
- messageId: 'useSafeArchiveExtraction',
281
- fix: () => null,
282
- },
283
- ],
284
- });
285
- }
286
- if (isDestDangerous && destArg) {
287
- context.report({
288
- node: destArg,
289
- messageId: 'dangerousArchiveDestination',
290
- data: {
291
- filePath: filename,
292
- line: String(node.loc?.start.line ?? 0),
293
- },
294
- });
295
- }
296
- }
297
- else {
298
- if (isDestDangerous) {
299
- context.report({
300
- node,
301
- messageId: 'dangerousArchiveDestination',
302
- data: {
303
- filePath: filename,
304
- line: String(node.loc?.start.line ?? 0),
305
- },
306
- });
307
- }
308
- else {
309
- context.report({
310
- node,
311
- messageId: 'unsafeArchiveExtraction',
312
- data: {
313
- filePath: filename,
314
- line: String(node.loc?.start.line ?? 0),
315
- },
316
- suggest: [
317
- {
318
- messageId: 'useSafeArchiveExtraction',
319
- fix: () => null
320
- },
321
- ],
322
- });
323
- }
324
- }
325
- }
326
- const callee = node.callee;
327
- if (callee.type === 'MemberExpression' &&
328
- callee.property.type === 'Identifier' &&
329
- ['join', 'resolve', 'relative', 'normalize'].includes(callee.property.name)) {
330
- const args = node.arguments;
331
- for (const arg of args) {
332
- if (arg.type === 'MemberExpression' &&
333
- arg.property.type === 'Identifier' &&
334
- ['name', 'path', 'fileName', 'entryName', 'relativePath', 'filename', 'pathname'].includes(arg.property.name)) {
335
- if (!isPathValidated(arg)) {
336
- context.report({
337
- node: arg,
338
- messageId: 'unvalidatedArchivePath',
339
- data: {
340
- filePath: filename,
341
- line: String(node.loc?.start.line ?? 0),
342
- },
343
- });
344
- }
345
- }
346
- }
347
- }
348
- },
349
- Literal(node) {
350
- if (typeof node.value !== 'string') {
351
- return;
352
- }
353
- const text = node.value;
354
- if ((text.includes('/') || text.includes('\\')) && containsPathTraversal(text)) {
355
- let current = node;
356
- let isArchiveContext = false;
357
- while (current) {
358
- if (current.type === 'CallExpression' && isArchiveExtraction(current)) {
359
- isArchiveContext = true;
360
- break;
361
- }
362
- if (current.type === 'VariableDeclarator' &&
363
- current.id.type === 'Identifier' &&
364
- (current.id.name.includes('archive') ||
365
- current.id.name.includes('zip') ||
366
- current.id.name.includes('tar') ||
367
- current.id.name.includes('path') ||
368
- current.id.name.includes('file') ||
369
- current.id.name.includes('entry'))) {
370
- isArchiveContext = true;
371
- break;
372
- }
373
- current = current.parent;
374
- }
375
- const parent = node.parent;
376
- if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
377
- const varName = parent.id.name.toLowerCase();
378
- if (varName.includes('archive') || varName.includes('zip') || varName.includes('tar') ||
379
- varName.includes('path') || varName.includes('file') || varName.includes('extract') ||
380
- varName.includes('entry')) {
381
- isArchiveContext = true;
382
- }
383
- }
384
- if (isArchiveContext) {
385
- context.report({
386
- node,
387
- messageId: 'pathTraversalInArchive',
388
- data: {
389
- filePath: filename,
390
- line: String(node.loc?.start.line ?? 0),
391
- },
392
- });
393
- }
394
- }
395
- },
396
- VariableDeclarator(node) {
397
- if (!node.init || node.id.type !== 'Identifier') {
398
- return;
399
- }
400
- const varName = node.id.name.toLowerCase();
401
- if (varName.includes('entry') || varName.includes('file') || varName.includes('path')) {
402
- if (node.init.type === 'MemberExpression' &&
403
- node.init.property.type === 'Identifier' &&
404
- ['name', 'path'].includes(node.init.property.name)) {
405
- }
406
- }
407
- }
408
- };
409
- },
410
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noZipSlip=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const containsPathTraversal=pathText=>{return/\.\.\//.test(pathText)||/\.\.\\/.test(pathText)||pathText.startsWith("..")||/\/\.\./.test(pathText)};const isDangerousDestination=destText=>{if(destText.startsWith("/tmp")||destText.includes("os.tmpdir")||destText.includes("TMPDIR")){return false}return destText.includes("/var")||destText.includes("/usr")||destText.includes("/etc")||destText.includes("/root")||destText.includes("/home")||destText.includes("C:\\Windows")||destText.includes("C:\\Program Files")||destText.includes("C:\\Users")};exports.noZipSlip=(0,eslint_devkit_1.createRule)({name:"no-zip-slip",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-zip-slip.md",description:"Detects zip slip/archive extraction vulnerabilities",cwe:"CWE-22"},hasSuggestions:true,messages:{zipSlipVulnerability:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Zip Slip Vulnerability",cwe:"CWE-22",description:"Archive extraction vulnerable to path traversal",severity:"{{severity}}",fix:"{{safeAlternative}}",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),unsafeArchiveExtraction:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unsafe Archive Extraction",cwe:"CWE-22",description:"Archive extraction without path validation",severity:"HIGH",fix:"Use safe extraction libraries or validate all paths",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),pathTraversalInArchive:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Path Traversal in Archive",cwe:"CWE-22",description:"Archive contains path traversal sequences",severity:"CRITICAL",fix:"Reject archives with path traversal or sanitize paths",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),unvalidatedArchivePath:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unvalidated Archive Path",cwe:"CWE-22",description:"Archive entry path used without validation",severity:"HIGH",fix:"Validate paths before extraction",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),dangerousArchiveDestination:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Dangerous Archive Destination",cwe:"CWE-22",description:"Archive extracted to sensitive location",severity:"MEDIUM",fix:"Extract to safe temporary directory",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),useSafeArchiveExtraction:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Use Safe Archive Extraction",description:"Use libraries with built-in path validation",severity:"LOW",fix:"Use yauzl, safe-archive-extract, or similar safe libraries",documentationLink:"https://www.npmjs.com/package/yauzl"}),validateArchivePaths:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Validate Archive Paths",description:"Validate all archive entry paths",severity:"LOW",fix:"Check paths don't contain ../ and are within destination directory",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),sanitizeArchiveNames:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Sanitize Archive Names",description:"Sanitize archive entry names",severity:"LOW",fix:"Use path.basename() or custom sanitization",documentationLink:"https://nodejs.org/api/path.html#pathbasenamepath-ext"}),strategyPathValidation:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Path Validation Strategy",description:"Validate paths before any file operations",severity:"LOW",fix:"Check path.startsWith(destination) and no ../ sequences",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),strategySafeLibraries:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Safe Libraries Strategy",description:"Use archive libraries with built-in safety",severity:"LOW",fix:"Use yauzl, adm-zip with validation, or safe-archive-extract",documentationLink:"https://www.npmjs.com/package/safe-archive-extract"}),strategySandboxing:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Sandboxing Strategy",description:"Extract archives in sandboxed environment",severity:"LOW",fix:"Use temporary directories and restrict permissions",documentationLink:"https://nodejs.org/api/fs.html#fsopentempdirprefix-options-callback"})},schema:[{type:"object",properties:{archiveFunctions:{type:"array",items:{type:"string"},default:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"]},pathValidationFunctions:{type:"array",items:{type:"string"},default:["validatePath","sanitizePath","checkPath","safePath"]},safeLibraries:{type:"array",items:{type:"string"},default:["yauzl","safe-archive-extract","tar-stream","unzipper"]}},additionalProperties:false}]},defaultOptions:[{archiveFunctions:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions:["validatePath","sanitizePath","checkPath","safePath"],safeLibraries:["yauzl","safe-archive-extract","tar-stream","unzipper"]}],create(context){const options=context.options[0]||{};const{archiveFunctions=["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions=["validatePath","sanitizePath","checkPath","safePath"],safeLibraries=["yauzl","safe-archive-extract","tar-stream","unzipper"]}=options;const filename=context.filename;const isArchiveExtraction=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&archiveFunctions.includes(callee.property.name)){return true}if(callee.type==="Identifier"&&archiveFunctions.includes(callee.name)){return true}return false};const isPathValidated=pathNode=>{let current=pathNode;while(current){if(current.type==="CallExpression"&&current.callee.type==="Identifier"&&pathValidationFunctions.includes(current.callee.name)){return true}if(current.type==="CallExpression"&&current.callee.type==="MemberExpression"&&current.callee.object.type==="Identifier"&&current.callee.object.name==="path"&&current.callee.property.type==="Identifier"&&current.callee.property.name==="basename"){return true}if(current.type==="IfStatement"){const test=current.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="startsWith"){return true}if(test.type==="UnaryExpression"&&test.operator==="!"&&test.argument.type==="CallExpression"&&test.argument.callee.type==="MemberExpression"&&test.argument.callee.property.type==="Identifier"&&test.argument.callee.property.name==="startsWith"){return true}if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){return true}}current=current.parent}return false};const isSafeLibrary=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.object.type==="Identifier"&&safeLibraries.includes(callee.object.name)){return true}if(callee.type==="Identifier"){const name=callee.name.toLowerCase();if(name==="extract"||name==="unzipper"||safeLibraries.some(lib=>name.includes(lib.toLowerCase()))){return true}}return false};return{CallExpression(node){if(isArchiveExtraction(node)&&!isSafeLibrary(node)){const sourceCode=context.sourceCode;let hasSafeAnnotation=false;const allComments=sourceCode.getAllComments();for(const comment of allComments){if(comment.type==="Block"&&comment.value.includes("@safe")){hasSafeAnnotation=true;break}}if(hasSafeAnnotation){return}const args=node.arguments;let destArg;if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(["extractAllTo","unzip"].includes(methodName)){destArg=args[0]}else{destArg=args.length>=2?args[1]:void 0}}else{destArg=args.length>=2?args[1]:void 0}const destText=destArg&&destArg.type==="Literal"&&typeof destArg.value==="string"?destArg.value:"";const isDestDangerous=isDangerousDestination(destText);const isMethodCall=node.callee.type==="MemberExpression";if(isMethodCall){const isSafeRelativePath=destText.startsWith("./")||destText.startsWith("../");if(!isSafeRelativePath){context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)},suggest:[{messageId:"useSafeArchiveExtraction",fix:()=>null}]})}if(isDestDangerous&&destArg){context.report({node:destArg,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}else{if(isDestDangerous){context.report({node,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}else{context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)},suggest:[{messageId:"useSafeArchiveExtraction",fix:()=>null}]})}}}const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&["join","resolve","relative","normalize"].includes(callee.property.name)){const args=node.arguments;for(const arg of args){if(arg.type==="MemberExpression"&&arg.property.type==="Identifier"&&["name","path","fileName","entryName","relativePath","filename","pathname"].includes(arg.property.name)){if(!isPathValidated(arg)){context.report({node:arg,messageId:"unvalidatedArchivePath",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}}},Literal(node){if(typeof node.value!=="string"){return}const text=node.value;if((text.includes("/")||text.includes("\\"))&&containsPathTraversal(text)){let current=node;let isArchiveContext=false;while(current){if(current.type==="CallExpression"&&isArchiveExtraction(current)){isArchiveContext=true;break}if(current.type==="VariableDeclarator"&&current.id.type==="Identifier"&&(current.id.name.includes("archive")||current.id.name.includes("zip")||current.id.name.includes("tar")||current.id.name.includes("path")||current.id.name.includes("file")||current.id.name.includes("entry"))){isArchiveContext=true;break}current=current.parent}const parent=node.parent;if(parent&&parent.type==="VariableDeclarator"&&parent.id.type==="Identifier"){const varName=parent.id.name.toLowerCase();if(varName.includes("archive")||varName.includes("zip")||varName.includes("tar")||varName.includes("path")||varName.includes("file")||varName.includes("extract")||varName.includes("entry")){isArchiveContext=true}}if(isArchiveContext){context.report({node,messageId:"pathTraversalInArchive",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}},VariableDeclarator(node){if(!node.init||node.id.type!=="Identifier"){return}const varName=node.id.name.toLowerCase();if(varName.includes("entry")||varName.includes("file")||varName.includes("path")){if(node.init.type==="MemberExpression"&&node.init.property.type==="Identifier"&&["name","path"].includes(node.init.property.name)){}}}}}});