eslint-plugin-node-security 4.13.0 → 4.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/package.json +2 -2
- package/src/index.js +1 -1
- package/src/rules/detect-child-process/index.js +1 -1
- package/src/rules/detect-suspicious-dependencies/index.js +1 -1
- package/src/rules/no-data-in-temp-storage/index.js +1 -1
- package/src/rules/no-dynamic-dependency-loading/index.js +1 -1
- package/src/rules/no-dynamic-require/index.js +1 -1
- package/src/rules/no-self-signed-certs/index.js +1 -1
- package/src/rules/no-timing-unsafe-compare/index.js +1 -1
- package/src/rules/no-toctou-vulnerability/index.js +1 -1
- package/src/rules/require-secure-credential-storage/index.js +1 -1
- package/src/rules/require-secure-deletion/index.js +1 -1
- package/src/rules/require-storage-encryption/index.js +1 -1
- package/src/utils/credential-evidence.js +1 -0
package/README.md
CHANGED
|
@@ -31,6 +31,45 @@ This plugin provides Security rules for Node.js core modules (fs, child_process,
|
|
|
31
31
|
|
|
32
32
|
**Interlace** fosters **strength through integration**. Instead of stacking isolated rules, we **interlace** security directly into your workflow to create a resilient fabric of code. We believe tools should **guide rather than gatekeep**, providing educational feedback that strengthens the developer with every interaction.
|
|
33
33
|
|
|
34
|
+
<!-- AUTO-GENERATED:DOCTRINE:START - Do not edit manually -->
|
|
35
|
+
|
|
36
|
+
## Why these rules are quiet
|
|
37
|
+
|
|
38
|
+
**Noise creates apathy, and apathy is not a security posture.** A linter that reports
|
|
39
|
+
a thousand things a week gets switched off in a month, and the real finding goes with
|
|
40
|
+
it. So every rule here is built to be worth reading: we would rather miss a finding
|
|
41
|
+
than spend your attention on one that was never real.
|
|
42
|
+
|
|
43
|
+
That is a trade, and it is made deliberately. It costs recall, and we measure what it
|
|
44
|
+
costs rather than assuming it is free.
|
|
45
|
+
|
|
46
|
+
## How the rules decide
|
|
47
|
+
|
|
48
|
+
**Evidence, not names.** A rule fires on what the code *does*, resolved through the
|
|
49
|
+
AST and ESLint's own scope analysis — not on an identifier that happens to contain
|
|
50
|
+
`query`, a method called `setItem`, or a file whose path contains `key`. Every one of
|
|
51
|
+
those was a real false positive in this ecosystem, found by reading our own output on
|
|
52
|
+
open-source projects and fixed with a test that fails on the unfixed rule.
|
|
53
|
+
|
|
54
|
+
Where a rule has known false-positive shapes, its page carries a **Not a finding**
|
|
55
|
+
section: what it deliberately stays quiet on, and what to check first when it fires
|
|
56
|
+
and you disagree.
|
|
57
|
+
|
|
58
|
+
## What you get
|
|
59
|
+
|
|
60
|
+
The rules below. Security rules carry a CWE mapping and, where one is assigned, a
|
|
61
|
+
CVSS score; every rule carries a fix on its message — in prose for a human and as
|
|
62
|
+
structured JSON for an agent. Install it, enable
|
|
63
|
+
`recommended`, and read the findings. If one of them is wrong,
|
|
64
|
+
[open an issue](https://github.com/ofri-peretz/eslint/issues) — a false positive is a
|
|
65
|
+
bug here, not a tuning exercise for you.
|
|
66
|
+
|
|
67
|
+
How that is measured, on which projects, and where it falls short:
|
|
68
|
+
[benchmark methodology](https://github.com/ofri-peretz/eslint/blob/main/BENCHMARK-METHODOLOGY.md)
|
|
69
|
+
and [results](https://github.com/ofri-peretz/eslint/blob/main/BENCHMARK-RESULTS.md).
|
|
70
|
+
|
|
71
|
+
<!-- AUTO-GENERATED:DOCTRINE:END -->
|
|
72
|
+
|
|
34
73
|
## Getting Started
|
|
35
74
|
|
|
36
75
|
- To check out the [guide](https://eslint.interlace.tools/docs/security/plugin-node-security?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security), visit [eslint.interlace.tools](https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security). 📚
|
|
@@ -124,7 +163,7 @@ See the [ESLint Version Support Policy](../../docs/ESLINT_VERSION_SUPPORT.md)
|
|
|
124
163
|
| [prefer-native-crypto](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/prefer-native-crypto?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-1104 | A06:2021 | | Prefer native crypto over third-party libraries | 🟢 | | | | | |
|
|
125
164
|
| [require-aead-tag-verification](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-aead-tag-verification?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-327 | A02:2021 | | Require AEAD decryption to verify the authentication tag (setAuthTag + final) | 🟢 | 💼 | | | | |
|
|
126
165
|
| [require-dependency-integrity](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-dependency-integrity?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-494 | | | CWE: [CWE-494](https://cwe.mitre.org/data/definitions/494.html) | 🟢 | 💼 | | | | |
|
|
127
|
-
| [require-secure-credential-storage](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-secure-credential-storage?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-312 | | | This rule detects
|
|
166
|
+
| [require-secure-credential-storage](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-secure-credential-storage?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-312 | | | This rule detects a credential written to localStorage, sessionStorage or AsyncStorage without encryption | 🟢 | | | | | |
|
|
128
167
|
| [require-secure-deletion](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-secure-deletion?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-459 | | | CWE: [CWE-459](https://cwe.mitre.org/data/definitions/459.html) | 🟢 | | | | | |
|
|
129
168
|
| [require-storage-encryption](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-storage-encryption?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-312 | | | CWE: [CWE-312](https://cwe.mitre.org/data/definitions/312.html) | 🟢 | | | | | |
|
|
130
169
|
| [require-stream-error-handler](https://eslint.interlace.tools/docs/security/plugin-node-security/rules/require-stream-error-handler?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-node-security) | CWE-248 | A04:2021 | | Require an error listener on streams passed to pipe, which does not forward errors | 🟢 | 💼 | | | | |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-node-security",
|
|
3
|
-
"version": "4.13.
|
|
3
|
+
"version": "4.13.1",
|
|
4
4
|
"description": "ESLint plugin for Node.js security — detects command injection, path traversal, SSRF, zip slip, and weak crypto (MD5/SHA-1, ECB, static IV) in fs, child_process, vm, and crypto.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"node": ">=18.0.0"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@interlace/eslint-devkit": "^1.
|
|
82
|
+
"@interlace/eslint-devkit": "^1.16.0"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|
|
85
85
|
"eslint": "^8.40.0 || ^9.0.0 || ^10.0.0"
|
package/src/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.configs=exports.plugin=exports.rules=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.rules={get"detect-child-process"(){return require("./rules/detect-child-process").detectChildProcess},get"detect-eval-with-expression"(){return require("./rules/detect-eval-with-expression").detectEvalWithExpression},get"detect-non-literal-fs-filename"(){return require("./rules/detect-non-literal-fs-filename").detectNonLiteralFsFilename},get"no-unsafe-dynamic-require"(){return require("./rules/no-unsafe-dynamic-require").noUnsafeDynamicRequire},get"no-buffer-overread"(){return require("./rules/no-buffer-overread").noBufferOverread},get"no-deprecated-buffer"(){return require("./rules/no-deprecated-buffer").noDeprecatedBuffer},get"no-unsafe-buffer-alloc"(){return require("./rules/no-unsafe-buffer-alloc").noUnsafeBufferAlloc},get"no-toctou-vulnerability"(){return require("./rules/no-toctou-vulnerability").noToctouVulnerability},get"no-zip-slip"(){return require("./rules/no-zip-slip").noZipSlip},get"no-arbitrary-file-access"(){return require("./rules/no-arbitrary-file-access").noArbitraryFileAccess},get"no-data-in-temp-storage"(){return require("./rules/no-data-in-temp-storage").noDataInTempStorage},get"no-ssrf"(){return require("./rules/no-ssrf").noSsrf},get"no-shell-injection"(){return require("./rules/no-shell-injection").noShellInjection},get"no-dynamic-command-string"(){return require("./rules/no-dynamic-command-string").noDynamicCommandString},get"no-env-injection"(){return require("./rules/no-env-injection").noEnvInjection},get"no-dynamic-algorithm-selection"(){return require("./rules/no-dynamic-algorithm-selection").noDynamicAlgorithmSelection},get"detect-suspicious-dependencies"(){return require("./rules/detect-suspicious-dependencies").detectSuspiciousDependencies},get"lock-file"(){return require("./rules/lock-file").lockFile},get"no-dynamic-dependency-loading"(){return require("./rules/no-dynamic-dependency-loading").noDynamicDependencyLoading},get"require-dependency-integrity"(){return require("./rules/require-dependency-integrity").requireDependencyIntegrity},get"require-secure-credential-storage"(){return require("./rules/require-secure-credential-storage").requireSecureCredentialStorage},get"require-secure-deletion"(){return require("./rules/require-secure-deletion").requireSecureDeletion},get"require-storage-encryption"(){return require("./rules/require-storage-encryption").requireStorageEncryption},get"no-dynamic-require"(){return require("./rules/no-dynamic-require").noDynamicRequire},get"no-cryptojs"(){return require("./rules/no-cryptojs").noCryptojs},get"no-cryptojs-weak-random"(){return require("./rules/no-cryptojs-weak-random").noCryptojsWeakRandom},get"no-deprecated-cipher-method"(){return require("./rules/no-deprecated-cipher-method").noDeprecatedCipherMethod},get"no-ecb-mode"(){return require("./rules/no-ecb-mode").noEcbMode},get"no-insecure-key-derivation"(){return require("./rules/no-insecure-key-derivation").noInsecureKeyDerivation},get"no-insecure-rsa-padding"(){return require("./rules/no-insecure-rsa-padding").noInsecureRsaPadding},get"no-math-random-crypto"(){return require("./rules/no-math-random-crypto").noMathRandomCrypto},get"no-self-signed-certs"(){return require("./rules/no-self-signed-certs").noSelfSignedCerts},get"no-sha1-hash"(){return require("./rules/no-sha1-hash").noSha1Hash},get"no-static-iv"(){return require("./rules/no-static-iv").noStaticIv},get"no-timing-unsafe-compare"(){return require("./rules/no-timing-unsafe-compare").noTimingUnsafeCompare},get"no-weak-cipher-algorithm"(){return require("./rules/no-weak-cipher-algorithm").noWeakCipherAlgorithm},get"no-weak-hash-algorithm"(){return require("./rules/no-weak-hash-algorithm").noWeakHashAlgorithm},get"prefer-native-crypto"(){return require("./rules/prefer-native-crypto").preferNativeCrypto},get"require-aead-tag-verification"(){return require("./rules/require-aead-tag-verification").requireAeadTagVerification},get"no-unbounded-decompression"(){return require("./rules/no-unbounded-decompression").noUnboundedDecompression},get"no-insecure-http-parser"(){return require("./rules/no-insecure-http-parser").noInsecureHttpParser},get"require-stream-error-handler"(){return require("./rules/require-stream-error-handler").requireStreamErrorHandler}};(0,eslint_devkit_1.withCanonicalDocsUrls)("plugin-node-security",exports.rules);exports.plugin={meta:{name:"eslint-plugin-node-security",version:"4.13.
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.configs=exports.plugin=exports.rules=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.rules={get"detect-child-process"(){return require("./rules/detect-child-process").detectChildProcess},get"detect-eval-with-expression"(){return require("./rules/detect-eval-with-expression").detectEvalWithExpression},get"detect-non-literal-fs-filename"(){return require("./rules/detect-non-literal-fs-filename").detectNonLiteralFsFilename},get"no-unsafe-dynamic-require"(){return require("./rules/no-unsafe-dynamic-require").noUnsafeDynamicRequire},get"no-buffer-overread"(){return require("./rules/no-buffer-overread").noBufferOverread},get"no-deprecated-buffer"(){return require("./rules/no-deprecated-buffer").noDeprecatedBuffer},get"no-unsafe-buffer-alloc"(){return require("./rules/no-unsafe-buffer-alloc").noUnsafeBufferAlloc},get"no-toctou-vulnerability"(){return require("./rules/no-toctou-vulnerability").noToctouVulnerability},get"no-zip-slip"(){return require("./rules/no-zip-slip").noZipSlip},get"no-arbitrary-file-access"(){return require("./rules/no-arbitrary-file-access").noArbitraryFileAccess},get"no-data-in-temp-storage"(){return require("./rules/no-data-in-temp-storage").noDataInTempStorage},get"no-ssrf"(){return require("./rules/no-ssrf").noSsrf},get"no-shell-injection"(){return require("./rules/no-shell-injection").noShellInjection},get"no-dynamic-command-string"(){return require("./rules/no-dynamic-command-string").noDynamicCommandString},get"no-env-injection"(){return require("./rules/no-env-injection").noEnvInjection},get"no-dynamic-algorithm-selection"(){return require("./rules/no-dynamic-algorithm-selection").noDynamicAlgorithmSelection},get"detect-suspicious-dependencies"(){return require("./rules/detect-suspicious-dependencies").detectSuspiciousDependencies},get"lock-file"(){return require("./rules/lock-file").lockFile},get"no-dynamic-dependency-loading"(){return require("./rules/no-dynamic-dependency-loading").noDynamicDependencyLoading},get"require-dependency-integrity"(){return require("./rules/require-dependency-integrity").requireDependencyIntegrity},get"require-secure-credential-storage"(){return require("./rules/require-secure-credential-storage").requireSecureCredentialStorage},get"require-secure-deletion"(){return require("./rules/require-secure-deletion").requireSecureDeletion},get"require-storage-encryption"(){return require("./rules/require-storage-encryption").requireStorageEncryption},get"no-dynamic-require"(){return require("./rules/no-dynamic-require").noDynamicRequire},get"no-cryptojs"(){return require("./rules/no-cryptojs").noCryptojs},get"no-cryptojs-weak-random"(){return require("./rules/no-cryptojs-weak-random").noCryptojsWeakRandom},get"no-deprecated-cipher-method"(){return require("./rules/no-deprecated-cipher-method").noDeprecatedCipherMethod},get"no-ecb-mode"(){return require("./rules/no-ecb-mode").noEcbMode},get"no-insecure-key-derivation"(){return require("./rules/no-insecure-key-derivation").noInsecureKeyDerivation},get"no-insecure-rsa-padding"(){return require("./rules/no-insecure-rsa-padding").noInsecureRsaPadding},get"no-math-random-crypto"(){return require("./rules/no-math-random-crypto").noMathRandomCrypto},get"no-self-signed-certs"(){return require("./rules/no-self-signed-certs").noSelfSignedCerts},get"no-sha1-hash"(){return require("./rules/no-sha1-hash").noSha1Hash},get"no-static-iv"(){return require("./rules/no-static-iv").noStaticIv},get"no-timing-unsafe-compare"(){return require("./rules/no-timing-unsafe-compare").noTimingUnsafeCompare},get"no-weak-cipher-algorithm"(){return require("./rules/no-weak-cipher-algorithm").noWeakCipherAlgorithm},get"no-weak-hash-algorithm"(){return require("./rules/no-weak-hash-algorithm").noWeakHashAlgorithm},get"prefer-native-crypto"(){return require("./rules/prefer-native-crypto").preferNativeCrypto},get"require-aead-tag-verification"(){return require("./rules/require-aead-tag-verification").requireAeadTagVerification},get"no-unbounded-decompression"(){return require("./rules/no-unbounded-decompression").noUnboundedDecompression},get"no-insecure-http-parser"(){return require("./rules/no-insecure-http-parser").noInsecureHttpParser},get"require-stream-error-handler"(){return require("./rules/require-stream-error-handler").requireStreamErrorHandler}};(0,eslint_devkit_1.withCanonicalDocsUrls)("plugin-node-security",exports.rules);exports.plugin={meta:{name:"eslint-plugin-node-security",version:"4.13.1"},rules:exports.rules};const recommendedRules={"node-security/detect-child-process":"error","node-security/detect-eval-with-expression":"error","node-security/detect-non-literal-fs-filename":"warn","node-security/no-unsafe-dynamic-require":"error","node-security/no-buffer-overread":"warn","node-security/no-deprecated-buffer":"error","node-security/no-unsafe-buffer-alloc":"warn","node-security/no-toctou-vulnerability":"error","node-security/no-zip-slip":"error","node-security/no-arbitrary-file-access":"error","node-security/no-data-in-temp-storage":"error","node-security/no-ssrf":"warn","node-security/no-shell-injection":"error","node-security/no-dynamic-command-string":"error","node-security/no-env-injection":"error","node-security/no-dynamic-algorithm-selection":"error","node-security/no-timing-unsafe-compare":"warn","node-security/detect-suspicious-dependencies":"warn","node-security/require-dependency-integrity":"error","node-security/no-weak-hash-algorithm":"error","node-security/no-weak-cipher-algorithm":"error","node-security/no-static-iv":"error","node-security/no-ecb-mode":"error","node-security/no-math-random-crypto":"error","node-security/no-cryptojs":"error","node-security/no-self-signed-certs":"error","node-security/require-aead-tag-verification":"error","node-security/no-unbounded-decompression":"error","node-security/no-insecure-http-parser":"error","node-security/require-stream-error-handler":"error"};exports.configs={recommended:{plugins:{"node-security":exports.plugin},rules:recommendedRules},strict:{plugins:{"node-security":exports.plugin},rules:Object.fromEntries(Object.keys(exports.rules).map(ruleName=>[`node-security/${ruleName}`,"error"]))}};exports.default=exports.plugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectChildProcess=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TAINT_SOURCES=["req","request","ctx","event","process"];const LOCAL_TAINT_ROOT="process";const SHELL_BINARIES=new Set(["sh","bash","zsh","dash","ksh","csh","tcsh","fish","ash","busybox","cmd","cmd.exe","powershell","powershell.exe","pwsh","pwsh.exe","env"]);const EVAL_FLAGS=new Set(["-c","-e","--eval","-e:","/c","/k","-command","-encodedcommand"]);const COMMAND_PATTERNS=[{method:"exec",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFile","spawn"],example:{bad:"exec(`git clone ${repoUrl}`)",good:["execFile('git', ['clone', repoUrl], {shell: false})","spawn('git', ['clone', repoUrl], {shell: false})"]},effort:"15-25 minutes"},{method:"execSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFileSync","spawnSync"],example:{bad:"execSync(`npm install ${packageName}`)",good:["execFileSync('npm', ['install', packageName], {shell: false})","spawnSync('npm', ['install', packageName], {shell: false})"]},effort:"15-25 minutes"},{method:"spawn",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawn with validation"],example:{bad:"spawn('bash', ['-c', userCommand])",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"20-30 minutes"},{method:"execFile",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"execFile(userCommand, userArgs, callback)",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"execFileSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"execFileSync(userCommand, userArgs)",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"spawnSync",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawnSync with validation"],example:{bad:"spawnSync('bash', ['-c', userCommand])",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"15-20 minutes"},{method:"fork",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"fork(userScript)",good:["spawn('node', [validatedScript], {shell: false})","// Validate script path first"]},effort:"15-20 minutes"},{method:"forkSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"forkSync(userScript)",good:["spawnSync('node', [validatedScript], {shell: false, stdio: 'inherit'})","// Validate script path first"]},effort:"15-20 minutes"}];const generateRefactoringSteps=pattern=>{switch(pattern.method){case"exec":case"execSync":return[" 1. Replace exec() with execFile() or spawn()"," 2. Split command and arguments into separate array elements"," 3. Use {shell: false} option to prevent shell interpretation"," 4. Validate and sanitize all user inputs"," 5. Consider using execa library for better security"].join("\n");case"spawn":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Consider using cross-spawn for cross-platform safety"].join("\n");case"execFile":return[" 1. Replace execFile() with spawn() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"execFileSync":return[" 1. Replace execFileSync() with spawnSync() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"spawnSync":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Handle synchronous execution properly"].join("\n");case"fork":return[" 1. Replace fork() with spawn() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawn('node', [scriptPath], options) instead"," 4. Add proper error handling"," 5. Consider using child_process.execFile() for simple scripts"].join("\n");case"forkSync":return[" 1. Replace forkSync() with spawnSync() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawnSync('node', [scriptPath], options) instead"," 4. Add proper error handling and synchronous waiting"," 5. Consider using child_process.execFileSync() for simple scripts"].join("\n");default:return[" 1. Identify the specific command execution need"," 2. Choose appropriate child_process method"," 3. Use argument arrays instead of string interpolation"," 4. Add comprehensive input validation"," 5. Test with malicious inputs"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectChildProcess=(0,eslint_devkit_2.createRule)({name:"detect-child-process",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-child-process.md",description:"Detects child_process usage that may allow command injection",cwe:"CWE-78",cvss:9.8,confidence:"medium"},hasSuggestions:true,messages:{childProcessCommandInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Command injection",cwe:"CWE-78",description:"Command injection detected",severity:"CRITICAL",fix:"Use execFile/spawn with {shell: false} and array args",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),argumentInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Argument injection",cwe:"CWE-88",description:"An attacker-steered value sits in the argv vector with no `--` before it. There is no shell here, but the callee still parses a leading `-` as an option \u2014 `--upload-pack=` (git), `--to-command=` (tar), `-o ProxyCommand=` (ssh) all execute arbitrary programs.",severity:"HIGH",fix:"Insert a literal '--' before the first attacker-controlled element, or reject values beginning with '-'.",documentationLink:"https://cwe.mitre.org/data/definitions/88.html"}),useEndOfOptions:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use end-of-options",description:"Add a literal '--' before user-controlled arguments",severity:"LOW",fix:"execFile('git', ['ls-remote', '--', remote])",documentationLink:"https://cwe.mitre.org/data/definitions/88.html"}),useExecFile:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use execFile",description:"Use execFile() with argument array",severity:"LOW",fix:"execFile(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback"}),useSpawn:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use spawn",description:"Use spawn() with separate arguments",severity:"LOW",fix:"spawn(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processspawncommand-args-options"}),useSaferLibrary:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safer Library",description:"Consider safer command execution libraries",severity:"LOW",fix:"Use execa, zx, or cross-spawn instead",documentationLink:"https://github.com/sindresorhus/execa"}),validateInput:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Input",description:"Add input validation and sanitization",severity:"LOW",fix:"Validate user input before passing to command",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),useShellFalse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Disable Shell",description:"Use shell: false option",severity:"LOW",fix:"{ shell: false } to prevent shell interpretation",documentationLink:"https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Validate Strategy",description:"Comprehensive input validation",severity:"LOW",fix:"Add allowlist validation before command execution",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategySanitize:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Sanitize Strategy",description:"Sanitize and escape command arguments",severity:"LOW",fix:"Escape special characters in command arguments",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategyRestrict:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Restrict Strategy",description:"Restrict to predefined safe commands",severity:"LOW",fix:"Define allowlist of permitted commands",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow exec() with literal strings"},allowLiteralSpawn:{type:"boolean",default:false,description:"Allow spawn() with literal arguments"},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional child_process methods to check"},strategy:{type:"string",enum:["validate","sanitize","restrict","auto"],default:"auto",description:"Strategy for fixing command injection (auto = smart detection)"},taintSources:{type:"array",items:{type:"string"},default:DEFAULT_TAINT_SOURCES,description:"Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process)"},reportUnresolvedCommands:{type:"boolean",default:false,description:'Report a command whose provenance cannot be resolved. Restores the pre-inversion "any dynamic argument is dangerous" behaviour.'}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,allowLiteralSpawn:false,additionalMethods:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,allowLiteralSpawn=false,additionalMethods=[]}=options;const taintRoots=new Set((options.taintSources??DEFAULT_TAINT_SOURCES).map(source=>source.toLowerCase()));const readsTaintSource=(0,provenance_1.makeReadsTaintSource)(context.sourceCode,taintRoots);const readsRemoteTaintSource=(0,provenance_1.makeReadsTaintSource)(context.sourceCode,new Set([...taintRoots].filter(root=>root!==LOCAL_TAINT_ROOT)));const reportUnresolvedCommands=options.reportUnresolvedCommands??false;const dangerousMethodsSet=new Set(["exec","execSync","execFile","execFileSync","spawn","spawnSync","fork","forkSync",...additionalMethods]);const isChildProcessSpecifier=value=>value==="child_process"||value==="node:child_process";const moduleAliases=new Set(["child_process"]);const importedMethods=new Set;const containsDynamicStrings=node=>!(0,eslint_devkit_2.isStaticExpression)({node,scope:context.sourceCode.getScope(node)});const isFreeReference=node=>{const name=node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node:node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node.callee:null;if(!name)return false;return context.sourceCode.getScope(name).through.some(ref=>ref.identifier===name&&ref.resolved===null)};const isStaticArg=argument=>(0,eslint_devkit_2.isStaticExpression)({node:argument,scope:context.sourceCode.getScope(argument)});const hasOnlyLiteralArgs=args=>{if(args.length===0)return false;if(!isStaticArg(args[0])){return false}if(args.length>=2){const argsArray=args[1];if(argsArray.type==="ArrayExpression"){const allLiteralElements=argsArray.elements.every(el=>el!==null&&isStaticArg(el));if(!allLiteralElements){return false}}else if(!isStaticArg(argsArray)){return false}}return true};const usesShell=(node,method)=>{if(method==="exec"||method==="execSync")return true;const command=node.arguments[0];if(command!==void 0&&command.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof command.value==="string"&&SHELL_BINARIES.has(command.value.replace(/^.*[/\\]/,"").toLowerCase())){return true}const argv=node.arguments[1];if(argv?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){for(const element of argv.elements){if(element?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof element.value==="string"&&EVAL_FLAGS.has(element.value.toLowerCase())){return true}}}for(const candidate of[node.arguments[1],node.arguments[2]]){if(!candidate||candidate.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)continue;for(const property of candidate.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.key.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||property.key.name!=="shell"){continue}return!(property.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value.value===false)}}return false};const cannotStartWithDash=node=>{if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const first=node.quasis[0].value.raw;return first.length>0&&!first.startsWith("-")}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"){const left=node.left;return left.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof left.value==="string"&&left.value.length>0&&!left.value.startsWith("-")}return false};const argumentInjectionSite=node=>{const argv=node.arguments[1];if(argv?.type!==eslint_devkit_1.AST_NODE_TYPES.ArrayExpression)return null;for(const element of argv.elements){if(element===null)continue;if(element.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&element.value==="--"){return null}if(!readsRemoteTaintSource(element))continue;const target=element.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement?element.argument:element;if(cannotStartWithDash(target))continue;return element}return null};const hasShellFalseOption=node=>{const optionsArg=node.arguments[2];if(!optionsArg||optionsArg.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){return true}for(const prop of optionsArg.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&prop.key.name==="shell"){if(prop.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&prop.value.value===false){return true}return false}}return true};const hasPrecedingAllowlistValidation=node=>{const makeArgChecker=validatedVarNames=>{const check=argNode=>{if(argNode.type==="Identifier"&&validatedVarNames.has(argNode.name))return true;if(argNode.type==="TemplateLiteral"){return argNode.expressions.some(e=>e.type==="Identifier"&&validatedVarNames.has(e.name))}if(argNode.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){return argNode.elements.some(el=>el!==null&&check(el))}return false};return check};const checkGuardClause=ifNode=>{const test=ifNode.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){const validatedVarNames=new Set;for(const testArg of test.arguments){if(testArg.type==="Identifier")validatedVarNames.add(testArg.name)}const check=makeArgChecker(validatedVarNames);for(const arg of node.arguments){if(check(arg))return true}}if(test.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&test.operator==="!"&&test.argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&test.argument.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&test.argument.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&test.argument.callee.property.name==="includes"){const consequent=ifNode.consequent;const isGuardBody=consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&&consequent.body.length>0&&(consequent.body[0].type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement||consequent.body[0].type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement);if(isGuardBody){const validatedVarNames=new Set;for(const testArg of test.argument.arguments){if(testArg.type==="Identifier")validatedVarNames.add(testArg.name)}const check=makeArgChecker(validatedVarNames.size>0?validatedVarNames:new Set(["*"]));if(validatedVarNames.size>0){for(const arg of node.arguments){if(check(arg))return true}}else{for(const arg of node.arguments){if(arg.type==="Identifier"||arg.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression&&arg.elements.some(el=>el?.type==="Identifier")){return true}}}}}return false};let current=node.parent;while(current){if(current.type==="IfStatement"){if(checkGuardClause(current))return true}current=current.parent}let stmt=node.parent;while(stmt&&stmt.parent&&stmt.parent.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement){stmt=stmt.parent}if(stmt&&stmt.parent&&stmt.parent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){const block=stmt.parent;const callIndex=block.body.indexOf(stmt);if(callIndex>0){for(let i=0;i<callIndex;i++){const sibling=block.body[i];if(sibling.type==="IfStatement"){if(checkGuardClause(sibling))return true}}}}return false};const extractCommandInfo=(node,method)=>{const sourceCode=context.sourceCode;const args=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=COMMAND_PATTERNS.find(p=>p.method===method)||null;const injectableArgs=node.arguments.slice(0,1);const argvArray=node.arguments[1];if(argvArray?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){injectableArgs.push(...argvArray.elements.filter(el=>el!==null))}const isDynamic=injectableArgs.some(arg=>containsDynamicStrings(arg));return{args,pattern,isDynamic}};const determineRiskLevel=(pattern,isDynamic)=>{if(pattern?.dangerous&&isDynamic){return"critical"}if(pattern?.dangerous||isDynamic){return"high"}return"medium"};const isChildProcessRequire=node=>node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&isChildProcessSpecifier(node.arguments[0].value);const getChildProcessCall=node=>{if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(!dangerousMethodsSet.has(methodName)){return null}if(node.callee.object.type==="Identifier"&&moduleAliases.has(node.callee.object.name)){return{method:methodName,calleeNode:node.callee}}if(isChildProcessRequire(node.callee.object)){return{method:methodName,calleeNode:node.callee}}}if(node.callee.type==="Identifier"&&dangerousMethodsSet.has(node.callee.name)){if(importedMethods.has(node.callee.name)){return{method:node.callee.name,calleeNode:node.callee}}}return null};const checkChildProcessCall=node=>{const detected=getChildProcessCall(node);if(!detected){return}const{method}=detected;const{args,pattern,isDynamic}=extractCommandInfo(node,method);if((method==="exec"||method==="execSync")&&!isDynamic&&hasOnlyLiteralArgs(node.arguments)){return}if(allowLiteralStrings&&method==="exec"&&!isDynamic){return}const saferMethods=new Set(["spawn","spawnSync","execFile","execFileSync"]);if(allowLiteralSpawn&&saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){return}if(saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){const isExecFile=method==="execFile"||method==="execFileSync";if(isExecFile||hasShellFalseOption(node)){return}}const allSafeMethods=["execFile","execFileSync","spawn","spawnSync"];if(allSafeMethods.includes(method)&&hasPrecedingAllowlistValidation(node)){return}const command=node.arguments[0];const commandIsSteerable=command===void 0||isFreeReference(command)||readsRemoteTaintSource(command);if(!usesShell(node,method)&&!commandIsSteerable){const injected=argumentInjectionSite(node);if(injected===null)return;context.report({node:injected,messageId:"argumentInjection",suggest:[{messageId:"useEndOfOptions",fix:()=>null},{messageId:"validateInput",fix:()=>null}]});return}const injectablePositions=node.arguments.slice(0,1);const argvVector=node.arguments[1];if(argvVector?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){injectablePositions.push(...argvVector.elements.filter(el=>el!==null))}const unknowable=injectablePositions.some(argument=>isFreeReference(argument));if(!reportUnresolvedCommands&&!unknowable&&!node.arguments.some(argument=>readsTaintSource(argument))){return}const riskLevel=determineRiskLevel(pattern,isDynamic);const steps=pattern?(0,exports.generateRefactoringSteps)(pattern):"Review and secure command execution";const alternatives=pattern?.safeAlternatives.join(", ")||"execFile, spawn with validation";context.report({node,messageId:"childProcessCommandInjection",data:{method,args,riskLevel,vulnerability:pattern?.vulnerability||"command injection",alternatives,steps,effort:pattern?.effort||"15-30 minutes"},suggest:[{messageId:"useExecFile",fix:()=>null},{messageId:"useSpawn",fix:()=>null},{messageId:"useSaferLibrary",fix:()=>null},{messageId:"validateInput",fix:()=>null},{messageId:"useShellFalse",fix:()=>null}]})};const trackChildProcessImport=node=>{if(!isChildProcessSpecifier(node.source.value)){return}for(const specifier of node.specifiers){if(specifier.type==="ImportDefaultSpecifier"||specifier.type==="ImportNamespaceSpecifier"){moduleAliases.add(specifier.local.name)}if(specifier.type==="ImportSpecifier"){importedMethods.add(specifier.local.name)}}};const trackChildProcessRequire=node=>{if(!node.init){return}if(node.id.type==="Identifier"&&node.init.type==="CallExpression"&&node.init.callee.type==="Identifier"&&node.init.callee.name==="require"&&node.init.arguments[0]&&node.init.arguments[0].type==="Literal"&&isChildProcessSpecifier(node.init.arguments[0].value)){moduleAliases.add(node.id.name);return}if(node.id.type==="ObjectPattern"&&node.init?.type==="CallExpression"&&node.init.callee.type==="Identifier"&&node.init.callee.name==="require"&&node.init.arguments[0]&&node.init.arguments[0].type==="Literal"&&isChildProcessSpecifier(node.init.arguments[0].value)){for(const prop of node.id.properties){if(prop.type==="Property"&&prop.key.type==="Identifier"){importedMethods.add(prop.value.type==="Identifier"?prop.value.name:prop.key.name)}}}};const checkBareChildProcessRequire=node=>{if(!isChildProcessRequire(node))return;const parent=node.parent;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.init===node)return;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===node)return;context.report({node,messageId:"childProcessCommandInjection",data:{method:"require",riskLevel:"MEDIUM",vulnerability:"command-injection",safeAlternatives:"execFile, spawn",refactoringSteps:" 1. Avoid importing child_process where it is not needed\n 2. If required, prefer execFile()/spawn() with {shell: false}\n 3. Validate any command or argument that is not a literal",effort:"10-15 minutes",badExample:"require('child_process')",goodExample:"const { execFile } = require('node:child_process')"}})};return{CallExpression(node){checkChildProcessCall(node);checkBareChildProcessRequire(node)},ImportDeclaration:trackChildProcessImport,VariableDeclarator:trackChildProcessRequire}}});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectChildProcess=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TAINT_SOURCES=["req","request","ctx","event","process"];const LOCAL_TAINT_ROOT="process";const SHELL_BINARIES=new Set(["sh","bash","zsh","dash","ksh","csh","tcsh","fish","ash","busybox","cmd","cmd.exe","powershell","powershell.exe","pwsh","pwsh.exe","env"]);const EVAL_FLAGS=new Set(["-c","-e","--eval","-e:","/c","/k","-command","-encodedcommand"]);const COMMAND_PATTERNS=[{method:"exec",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFile","spawn"],example:{bad:"exec(`git clone ${repoUrl}`)",good:["execFile('git', ['clone', repoUrl], {shell: false})","spawn('git', ['clone', repoUrl], {shell: false})"]},effort:"15-25 minutes"},{method:"execSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFileSync","spawnSync"],example:{bad:"execSync(`npm install ${packageName}`)",good:["execFileSync('npm', ['install', packageName], {shell: false})","spawnSync('npm', ['install', packageName], {shell: false})"]},effort:"15-25 minutes"},{method:"spawn",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawn with validation"],example:{bad:"spawn('bash', ['-c', userCommand])",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"20-30 minutes"},{method:"execFile",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"execFile(userCommand, userArgs, callback)",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"execFileSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"execFileSync(userCommand, userArgs)",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"spawnSync",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawnSync with validation"],example:{bad:"spawnSync('bash', ['-c', userCommand])",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"15-20 minutes"},{method:"fork",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"fork(userScript)",good:["spawn('node', [validatedScript], {shell: false})","// Validate script path first"]},effort:"15-20 minutes"},{method:"forkSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"forkSync(userScript)",good:["spawnSync('node', [validatedScript], {shell: false, stdio: 'inherit'})","// Validate script path first"]},effort:"15-20 minutes"}];const generateRefactoringSteps=pattern=>{switch(pattern.method){case"exec":case"execSync":return[" 1. Replace exec() with execFile() or spawn()"," 2. Split command and arguments into separate array elements"," 3. Use {shell: false} option to prevent shell interpretation"," 4. Validate and sanitize all user inputs"," 5. Consider using execa library for better security"].join("\n");case"spawn":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Consider using cross-spawn for cross-platform safety"].join("\n");case"execFile":return[" 1. Replace execFile() with spawn() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"execFileSync":return[" 1. Replace execFileSync() with spawnSync() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"spawnSync":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Handle synchronous execution properly"].join("\n");case"fork":return[" 1. Replace fork() with spawn() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawn('node', [scriptPath], options) instead"," 4. Add proper error handling"," 5. Consider using child_process.execFile() for simple scripts"].join("\n");case"forkSync":return[" 1. Replace forkSync() with spawnSync() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawnSync('node', [scriptPath], options) instead"," 4. Add proper error handling and synchronous waiting"," 5. Consider using child_process.execFileSync() for simple scripts"].join("\n");default:return[" 1. Identify the specific command execution need"," 2. Choose appropriate child_process method"," 3. Use argument arrays instead of string interpolation"," 4. Add comprehensive input validation"," 5. Test with malicious inputs"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectChildProcess=(0,eslint_devkit_2.createRule)({name:"detect-child-process",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-child-process.md",description:"Detects child_process usage that may allow command injection",cwe:"CWE-78",cvss:9.8,confidence:"medium"},hasSuggestions:true,messages:{childProcessCommandInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Command injection",cwe:"CWE-78",description:"Command injection detected",severity:"CRITICAL",fix:"Use execFile/spawn with {shell: false} and array args",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),argumentInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Argument injection",cwe:"CWE-88",description:"An attacker-steered value sits in the argv vector with no `--` before it. There is no shell here, but the callee still parses a leading `-` as an option \u2014 `--upload-pack=` (git), `--to-command=` (tar), `-o ProxyCommand=` (ssh) all execute arbitrary programs.",severity:"HIGH",fix:"Insert a literal '--' before the first attacker-controlled element, or reject values beginning with '-'.",documentationLink:"https://cwe.mitre.org/data/definitions/88.html"}),useEndOfOptions:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use end-of-options",description:"Add a literal '--' before user-controlled arguments",severity:"LOW",fix:"execFile('git', ['ls-remote', '--', remote])",documentationLink:"https://cwe.mitre.org/data/definitions/88.html"}),useExecFile:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use execFile",description:"Use execFile() with argument array",severity:"LOW",fix:"execFile(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback"}),useSpawn:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use spawn",description:"Use spawn() with separate arguments",severity:"LOW",fix:"spawn(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processspawncommand-args-options"}),useSaferLibrary:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safer Library",description:"Consider safer command execution libraries",severity:"LOW",fix:"Use execa, zx, or cross-spawn instead",documentationLink:"https://github.com/sindresorhus/execa"}),validateInput:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Input",description:"Add input validation and sanitization",severity:"LOW",fix:"Validate user input before passing to command",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),useShellFalse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Disable Shell",description:"Use shell: false option",severity:"LOW",fix:"{ shell: false } to prevent shell interpretation",documentationLink:"https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Validate Strategy",description:"Comprehensive input validation",severity:"LOW",fix:"Add allowlist validation before command execution",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategySanitize:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Sanitize Strategy",description:"Sanitize and escape command arguments",severity:"LOW",fix:"Escape special characters in command arguments",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategyRestrict:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Restrict Strategy",description:"Restrict to predefined safe commands",severity:"LOW",fix:"Define allowlist of permitted commands",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow exec() with literal strings"},allowLiteralSpawn:{type:"boolean",default:false,description:"Allow spawn() with literal arguments"},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional child_process methods to check"},strategy:{type:"string",enum:["validate","sanitize","restrict","auto"],default:"auto",description:"Strategy for fixing command injection (auto = smart detection)"},taintSources:{type:"array",items:{type:"string"},default:DEFAULT_TAINT_SOURCES,description:"Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process)"},reportUnresolvedCommands:{type:"boolean",default:false,description:'Report a command whose provenance cannot be resolved. Restores the pre-inversion "any dynamic argument is dangerous" behaviour.'}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,allowLiteralSpawn:false,additionalMethods:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,allowLiteralSpawn=false,additionalMethods=[]}=options;const taintRoots=new Set((options.taintSources??DEFAULT_TAINT_SOURCES).map(source=>source.toLowerCase()));const readsTaintSource=(0,provenance_1.makeReadsTaintSource)(context.sourceCode,taintRoots);const readsRemoteTaintSource=(0,provenance_1.makeReadsTaintSource)(context.sourceCode,new Set([...taintRoots].filter(root=>root!==LOCAL_TAINT_ROOT)));const reportUnresolvedCommands=options.reportUnresolvedCommands??false;const dangerousMethodsSet=new Set(["exec","execSync","execFile","execFileSync","spawn","spawnSync","fork","forkSync",...additionalMethods]);const isChildProcessSpecifier=value=>value==="child_process"||value==="node:child_process";const moduleAliases=new Set(["child_process"]);const importedMethods=new Set;const containsDynamicStrings=node=>!(0,eslint_devkit_2.isStaticExpression)({node,scope:context.sourceCode.getScope(node)});const isFreeReference=node=>{const name=node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node:node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node.callee:null;if(!name)return false;return context.sourceCode.getScope(name).through.some(ref=>ref.identifier===name&&ref.resolved===null)};const isStaticArg=argument=>(0,eslint_devkit_2.isStaticExpression)({node:argument,scope:context.sourceCode.getScope(argument)});const hasOnlyLiteralArgs=args=>{if(args.length===0)return false;if(!isStaticArg(args[0])){return false}if(args.length>=2){const argsArray=args[1];if(argsArray.type==="ArrayExpression"){const allLiteralElements=argsArray.elements.every(el=>el!==null&&isStaticArg(el));if(!allLiteralElements){return false}}else if(!isStaticArg(argsArray)){return false}}return true};const usesShell=(node,method)=>{if(method==="exec"||method==="execSync")return true;const command=node.arguments[0];if(command!==void 0&&command.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof command.value==="string"&&SHELL_BINARIES.has(command.value.replace(/^.*[/\\]/,"").toLowerCase())){return true}const argv=node.arguments[1];if(argv?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){for(const element of argv.elements){if(element?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof element.value==="string"&&EVAL_FLAGS.has(element.value.toLowerCase())){return true}}}for(const candidate of[node.arguments[1],node.arguments[2]]){if(!candidate||candidate.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)continue;for(const property of candidate.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.key.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||property.key.name!=="shell"){continue}return!(property.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value.value===false)}}return false};const cannotStartWithDash=node=>{if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const first=node.quasis[0].value.raw;return first.length>0&&!first.startsWith("-")}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"){const left=node.left;return left.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof left.value==="string"&&left.value.length>0&&!left.value.startsWith("-")}return false};const argumentInjectionSite=node=>{const argv=node.arguments[1];if(argv?.type!==eslint_devkit_1.AST_NODE_TYPES.ArrayExpression)return null;for(const element of argv.elements){if(element===null)continue;if(element.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&element.value==="--"){return null}if(!readsRemoteTaintSource(element))continue;const target=element.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement?element.argument:element;if(cannotStartWithDash(target))continue;return element}return null};const hasShellFalseOption=node=>{const optionsArg=node.arguments[2];if(!optionsArg||optionsArg.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){return true}for(const prop of optionsArg.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&prop.key.name==="shell"){if(prop.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&prop.value.value===false){return true}return false}}return true};const hasPrecedingAllowlistValidation=node=>{const makeArgChecker=validatedVarNames=>{const check=argNode=>{if(argNode.type==="Identifier"&&validatedVarNames.has(argNode.name))return true;if(argNode.type==="TemplateLiteral"){return argNode.expressions.some(e=>e.type==="Identifier"&&validatedVarNames.has(e.name))}if(argNode.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){return argNode.elements.some(el=>el!==null&&check(el))}return false};return check};const checkGuardClause=ifNode=>{const test=ifNode.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){const validatedVarNames=new Set;for(const testArg of test.arguments){if(testArg.type==="Identifier")validatedVarNames.add(testArg.name)}const check=makeArgChecker(validatedVarNames);for(const arg of node.arguments){if(check(arg))return true}}if(test.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&test.operator==="!"&&test.argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&test.argument.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&test.argument.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&test.argument.callee.property.name==="includes"){const consequent=ifNode.consequent;const isGuardBody=consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&&consequent.body.length>0&&(consequent.body[0].type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement||consequent.body[0].type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement);if(isGuardBody){const validatedVarNames=new Set;for(const testArg of test.argument.arguments){if(testArg.type==="Identifier")validatedVarNames.add(testArg.name)}const check=makeArgChecker(validatedVarNames.size>0?validatedVarNames:new Set(["*"]));if(validatedVarNames.size>0){for(const arg of node.arguments){if(check(arg))return true}}else{for(const arg of node.arguments){if(arg.type==="Identifier"||arg.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression&&arg.elements.some(el=>el?.type==="Identifier")){return true}}}}}return false};let current=node.parent;while(current){if(current.type==="IfStatement"){if(checkGuardClause(current))return true}current=current.parent}let stmt=node.parent;while(stmt&&stmt.parent&&stmt.parent.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement){stmt=stmt.parent}if(stmt&&stmt.parent&&stmt.parent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){const block=stmt.parent;const callIndex=block.body.indexOf(stmt);if(callIndex>0){for(let i=0;i<callIndex;i++){const sibling=block.body[i];if(sibling.type==="IfStatement"){if(checkGuardClause(sibling))return true}}}}return false};const extractCommandInfo=(node,method)=>{const sourceCode=context.sourceCode;const args=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=COMMAND_PATTERNS.find(p=>p.method===method)||null;const injectableArgs=node.arguments.slice(0,1);const argvArray=node.arguments[1];if(argvArray?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){injectableArgs.push(...argvArray.elements.filter(el=>el!==null))}const isDynamic=injectableArgs.some(arg=>containsDynamicStrings(arg));return{args,pattern,isDynamic}};const determineRiskLevel=(pattern,isDynamic)=>{if(pattern?.dangerous&&isDynamic){return"critical"}if(pattern?.dangerous||isDynamic){return"high"}return"medium"};const isChildProcessRequire=node=>node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&isChildProcessSpecifier(node.arguments[0].value);const childProcessMemberName=node=>{if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;for(let scope=context.sourceCode.getScope(node);scope;scope=scope.upper){const variable=scope.variables.find(v=>v.name===node.name);if(!variable)continue;const[def]=variable.defs;if(def?.type==="ImportBinding"){const declaration=def.parent;if(declaration?.type!==eslint_devkit_1.AST_NODE_TYPES.ImportDeclaration||!isChildProcessSpecifier(declaration.source.value)||def.node.type!==eslint_devkit_1.AST_NODE_TYPES.ImportSpecifier){return null}return def.node.imported.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?def.node.imported.name:String(def.node.imported.value)}if(def?.type==="Variable"){const declarator=def.node;if(declarator.init==null||!isChildProcessRequire(declarator.init)||declarator.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern){return null}for(const property of declarator.id.properties){if(property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&property.value.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.value.name===node.name&&property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return property.key.name}}}return null}return null};const resolvesToChildProcess=(node,fallback)=>{if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;for(let scope=context.sourceCode.getScope(node);scope;scope=scope.upper){const variable=scope.variables.find(v=>v.name===node.name);if(!variable)continue;const[def]=variable.defs;if(def?.type==="ImportBinding"){const declaration=def.parent;return declaration?.type===eslint_devkit_1.AST_NODE_TYPES.ImportDeclaration&&isChildProcessSpecifier(declaration.source.value)}if(def?.type==="Variable"){return def.node.init!=null&&isChildProcessRequire(def.node.init)}return false}return fallback.has(node.name)};const getChildProcessCall=node=>{if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(!dangerousMethodsSet.has(methodName)){return null}if(resolvesToChildProcess(node.callee.object,moduleAliases)){return{method:methodName,calleeNode:node.callee}}if(isChildProcessRequire(node.callee.object)){return{method:methodName,calleeNode:node.callee}}}const member=childProcessMemberName(node.callee);if(member!==null&&dangerousMethodsSet.has(member)){return{method:member,calleeNode:node.callee}}if(node.callee.type==="Identifier"&&dangerousMethodsSet.has(node.callee.name)){if(resolvesToChildProcess(node.callee,importedMethods)){return{method:node.callee.name,calleeNode:node.callee}}}return null};const checkChildProcessCall=node=>{const detected=getChildProcessCall(node);if(!detected){return}const{method}=detected;const{args,pattern,isDynamic}=extractCommandInfo(node,method);if((method==="exec"||method==="execSync")&&!isDynamic&&hasOnlyLiteralArgs(node.arguments)){return}if(allowLiteralStrings&&method==="exec"&&!isDynamic){return}const saferMethods=new Set(["spawn","spawnSync","execFile","execFileSync"]);if(allowLiteralSpawn&&saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){return}if(saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){const isExecFile=method==="execFile"||method==="execFileSync";if(isExecFile||hasShellFalseOption(node)){return}}const allSafeMethods=["execFile","execFileSync","spawn","spawnSync"];if(allSafeMethods.includes(method)&&hasPrecedingAllowlistValidation(node)){return}const command=node.arguments[0];const commandIsSteerable=command===void 0||isFreeReference(command)||readsRemoteTaintSource(command);if(!usesShell(node,method)&&!commandIsSteerable){const injected=argumentInjectionSite(node);if(injected===null)return;context.report({node:injected,messageId:"argumentInjection",suggest:[{messageId:"useEndOfOptions",fix:()=>null},{messageId:"validateInput",fix:()=>null}]});return}const injectablePositions=node.arguments.slice(0,1);const argvVector=node.arguments[1];if(argvVector?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){injectablePositions.push(...argvVector.elements.filter(el=>el!==null))}const unknowable=injectablePositions.some(argument=>isFreeReference(argument));if(!reportUnresolvedCommands&&!unknowable&&!node.arguments.some(argument=>readsTaintSource(argument))){return}const riskLevel=determineRiskLevel(pattern,isDynamic);const steps=pattern?(0,exports.generateRefactoringSteps)(pattern):"Review and secure command execution";const alternatives=pattern?.safeAlternatives.join(", ")||"execFile, spawn with validation";context.report({node,messageId:"childProcessCommandInjection",data:{method,args,riskLevel,vulnerability:pattern?.vulnerability||"command injection",alternatives,steps,effort:pattern?.effort||"15-30 minutes"},suggest:[{messageId:"useExecFile",fix:()=>null},{messageId:"useSpawn",fix:()=>null},{messageId:"useSaferLibrary",fix:()=>null},{messageId:"validateInput",fix:()=>null},{messageId:"useShellFalse",fix:()=>null}]})};const trackChildProcessImport=node=>{if(!isChildProcessSpecifier(node.source.value)){return}for(const specifier of node.specifiers){if(specifier.type==="ImportDefaultSpecifier"||specifier.type==="ImportNamespaceSpecifier"){moduleAliases.add(specifier.local.name)}if(specifier.type==="ImportSpecifier"){importedMethods.add(specifier.local.name)}}};const trackChildProcessRequire=node=>{if(!node.init){return}if(node.id.type==="Identifier"&&node.init.type==="CallExpression"&&node.init.callee.type==="Identifier"&&node.init.callee.name==="require"&&node.init.arguments[0]&&node.init.arguments[0].type==="Literal"&&isChildProcessSpecifier(node.init.arguments[0].value)){moduleAliases.add(node.id.name);return}if(node.id.type==="ObjectPattern"&&node.init?.type==="CallExpression"&&node.init.callee.type==="Identifier"&&node.init.callee.name==="require"&&node.init.arguments[0]&&node.init.arguments[0].type==="Literal"&&isChildProcessSpecifier(node.init.arguments[0].value)){for(const prop of node.id.properties){if(prop.type==="Property"&&prop.key.type==="Identifier"){importedMethods.add(prop.value.type==="Identifier"?prop.value.name:prop.key.name)}}}};const checkBareChildProcessRequire=node=>{if(!isChildProcessRequire(node))return;const parent=node.parent;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.init===node)return;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===node)return;context.report({node,messageId:"childProcessCommandInjection",data:{method:"require",riskLevel:"MEDIUM",vulnerability:"command-injection",safeAlternatives:"execFile, spawn",refactoringSteps:" 1. Avoid importing child_process where it is not needed\n 2. If required, prefer execFile()/spawn() with {shell: false}\n 3. Validate any command or argument that is not a literal",effort:"10-15 minutes",badExample:"require('child_process')",goodExample:"const { execFile } = require('node:child_process')"}})};return{CallExpression(node){checkChildProcessCall(node);checkBareChildProcessRequire(node)},ImportDeclaration:trackChildProcessImport,VariableDeclarator:trackChildProcessRequire}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectSuspiciousDependencies=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.detectSuspiciousDependencies=(0,eslint_devkit_1.createRule)({name:"detect-suspicious-dependencies",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-suspicious-dependencies.md",description:"Detect typosquatting in package names",cwe:"CWE-506",cvss:
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectSuspiciousDependencies=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.detectSuspiciousDependencies=(0,eslint_devkit_1.createRule)({name:"detect-suspicious-dependencies",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-suspicious-dependencies.md",description:"Detect typosquatting in package names",cwe:"CWE-506",cvss:9.8},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Suspicious Dependency",cwe:"CWE-506",description:"Suspicious package name detected - possible typosquatting",severity:"HIGH",fix:"Verify package authenticity on npm registry",documentationLink:"https://cwe.mitre.org/data/definitions/506.html"})},schema:[]},defaultOptions:[],create(context){const popularPackages=["react","lodash","express","axios","webpack"];const KNOWN_LEGITIMATE=new Set(["preact","recast","react-dom","reactor","redux","lodash-es","expressive","axios-retry","webpack-cli"]);function levenshtein(a,b){const matrix=[];for(let i=0;i<=b.length;i++){matrix[i]=[i]}for(let j=0;j<=a.length;j++){matrix[0][j]=j}for(let i=1;i<=b.length;i++){for(let j=1;j<=a.length;j++){if(b.charAt(i-1)===a.charAt(j-1)){matrix[i][j]=matrix[i-1][j-1]}else{matrix[i][j]=Math.min(matrix[i-1][j-1]+1,matrix[i][j-1]+1,matrix[i-1][j]+1);if(i>1&&j>1&&b.charAt(i-1)===a.charAt(j-2)&&b.charAt(i-2)===a.charAt(j-1)){matrix[i][j]=Math.min(matrix[i][j],matrix[i-2][j-2]+1)}}}}return matrix[b.length][a.length]}const checkSpecifier=(node,source)=>{if(typeof source!=="string")return;if(source.startsWith(".")||source.startsWith("@"))return;for(const popular of popularPackages){const distance=levenshtein(source,popular);if(distance===1&&!KNOWN_LEGITIMATE.has(source)){context.report({node,messageId:"violationDetected",data:{name:source,similar:popular}})}}};return{ImportDeclaration(node){checkSpecifier(node,node.source.value)},TSImportEqualsDeclaration(node){const ref=node.moduleReference;if(ref.type!==eslint_devkit_1.AST_NODE_TYPES.TSExternalModuleReference)return;checkSpecifier(node,ref.expression.value)},ImportExpression(node){if(node.source.type!==eslint_devkit_1.AST_NODE_TYPES.Literal)return;checkSpecifier(node,node.source.value)},CallExpression(node){if(node.callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return;if(node.callee.name!=="require")return;const[arg]=node.arguments;if(arg?.type!==eslint_devkit_1.AST_NODE_TYPES.Literal)return;checkSpecifier(node,arg.value)}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDataInTempStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TEMP_PATHS=["/tmp","/var/tmp","temp/","/temp"];const FS_WRITE_FUNCTIONS=["writeFileSync","writeFile"];function containsPathSegments(haystack,needle){const split=value=>value.split(/[/\\]/).filter(segment=>segment.length>0);const target=split(needle);if(target.length===0)return false;const segments=split(haystack);for(let start=0;start+target.length<=segments.length;start+=1){if(target.every((segment,offset)=>segments[start+offset]===segment))return true}return false}function isTmpdirCall(node){return node.type==="CallExpression"&&node.callee.type==="MemberExpression"&&!node.callee.computed&&node.callee.object.type==="Identifier"&&node.callee.object.name==="os"&&node.callee.property.type==="Identifier"&&node.callee.property.name==="tmpdir"}function isStaticTmpdirJoin(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return false;if(callee.property.type!=="Identifier")return false;if(callee.property.name!=="join"&&callee.property.name!=="resolve"){return false}if(callee.object.type!=="Identifier"||callee.object.name!=="path"){return false}if(node.arguments.length<2)return false;if(!isTmpdirCall(node.arguments[0]))return false;return node.arguments.slice(1).every(arg=>arg.type==="Literal"&&typeof arg.value==="string")}exports.noDataInTempStorage=(0,eslint_devkit_1.createRule)({name:"no-data-in-temp-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-data-in-temp-storage.md",description:"Prevent sensitive data in temp directories",cwe:"CWE-312",cvss:
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDataInTempStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TEMP_PATHS=["/tmp","/var/tmp","temp/","/temp"];const FS_WRITE_FUNCTIONS=["writeFileSync","writeFile"];function containsPathSegments(haystack,needle){const split=value=>value.split(/[/\\]/).filter(segment=>segment.length>0);const target=split(needle);if(target.length===0)return false;const segments=split(haystack);for(let start=0;start+target.length<=segments.length;start+=1){if(target.every((segment,offset)=>segments[start+offset]===segment))return true}return false}function isTmpdirCall(node){return node.type==="CallExpression"&&node.callee.type==="MemberExpression"&&!node.callee.computed&&node.callee.object.type==="Identifier"&&node.callee.object.name==="os"&&node.callee.property.type==="Identifier"&&node.callee.property.name==="tmpdir"}function isStaticTmpdirJoin(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return false;if(callee.property.type!=="Identifier")return false;if(callee.property.name!=="join"&&callee.property.name!=="resolve"){return false}if(callee.object.type!=="Identifier"||callee.object.name!=="path"){return false}if(node.arguments.length<2)return false;if(!isTmpdirCall(node.arguments[0]))return false;return node.arguments.slice(1).every(arg=>arg.type==="Literal"&&typeof arg.value==="string")}exports.noDataInTempStorage=(0,eslint_devkit_1.createRule)({name:"no-data-in-temp-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-data-in-temp-storage.md",description:"Prevent sensitive data in temp directories",cwe:"CWE-312",cvss:5.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Temp Storage Data",cwe:"CWE-312",description:"Sensitive data written to temp directory - not secure",severity:"HIGH",fix:"Use secure storage location or encrypt data before writing",documentationLink:"https://cwe.mitre.org/data/definitions/312.html"}),predictableTempPath:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Predictable Temp File Name (CWE-377)",cwe:"CWE-377",cvss:7.5,description:"path.join(os.tmpdir(), \u2026) with a constant name resolves to the same path on every run, in a directory every local user can write. An attacker who pre-creates that name \u2014 or symlinks it at a file they want clobbered \u2014 wins the race before the write happens.",severity:"HIGH",fix:"Create the file inside a fresh directory from fs.mkdtemp/mkdtempSync, or add a crypto.randomUUID() segment to the name.",documentationLink:"https://cwe.mitre.org/data/definitions/377.html"})},schema:[{type:"object",properties:{tempPaths:{type:"array",items:{type:"string"},description:"Custom list of temporary paths to flag"},ignoreFiles:{type:"array",items:{type:"string"},description:"List of files or patterns to ignore"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const options=context.options[0]||{};const tempPaths=options.tempPaths||DEFAULT_TEMP_PATHS;const ignoreFiles=options.ignoreFiles||[];const filename=context.filename;if(ignoreFiles.some(pattern=>filename.includes(pattern))){return{}}function report(node){context.report({node,messageId:"violationDetected"})}function reportIfPredictable(candidate){if(!candidate||candidate.type!=="CallExpression")return;if(!isStaticTmpdirJoin(candidate))return;context.report({node:candidate,messageId:"predictableTempPath"})}function isFsWriteCall(node){return node?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.callee.computed&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.object.name==="fs"&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&FS_WRITE_FUNCTIONS.includes(node.callee.property.name)}function isWrittenThrough(identifier){const variable=(0,provenance_1.findVariable)(context.sourceCode,identifier);if(!variable)return false;return variable.references.some(reference=>{const parent=reference.identifier.parent;return isFsWriteCall(parent)&&parent.arguments[0]===reference.identifier})}function boundName(parent){if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return parent.id}if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&parent.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return parent.left}return null}return{CallExpression(node){if(!isFsWriteCall(node))return;const pathArg=node.arguments[0];if(pathArg?.type==="Literal"&&typeof pathArg.value==="string"){const value=pathArg.value;if(tempPaths.some(tp=>containsPathSegments(value,tp))){report(pathArg)}}reportIfPredictable(pathArg)},VariableDeclarator(node){reportIfPredictable(node.init)},AssignmentExpression(node){reportIfPredictable(node.right)},Literal(node){const value=node.value;if(typeof value!=="string")return;if(!tempPaths.some(tp=>containsPathSegments(value,tp)))return;const name=boundName(node.parent);if(name!==null&&isWrittenThrough(name))report(node)}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicDependencyLoading=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noDynamicDependencyLoading=(0,eslint_devkit_1.createRule)({name:"no-dynamic-dependency-loading",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-dependency-loading.md",description:"Prevent runtime dependency injection with dynamic paths",cwe:"CWE-1104",cvss:5.3},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"violation Detected",cwe:"CWE-1104",description:"Dynamic import/require detected - use static imports for security",severity:"HIGH",fix:"Review and apply secure practices",documentationLink:"https://cwe.mitre.org/data/definitions/1104.html"})},schema:[]},defaultOptions:[],create(context){return{CallExpression(node){if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicDependencyLoading=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noDynamicDependencyLoading=(0,eslint_devkit_1.createRule)({name:"no-dynamic-dependency-loading",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-dependency-loading.md",description:"Prevent runtime dependency injection with dynamic paths",cwe:"CWE-1104",cvss:5.3},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"violation Detected",cwe:"CWE-1104",description:"Dynamic import/require detected - use static imports for security",severity:"HIGH",fix:"Review and apply secure practices",documentationLink:"https://cwe.mitre.org/data/definitions/1104.html"})},schema:[]},defaultOptions:[],create(context){const isSteerable=node=>!(0,eslint_devkit_1.isStaticExpression)({node,scope:context.sourceCode.getScope(node)});return{CallExpression(node){const specifier=node.arguments[0];if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&specifier!==void 0&&isSteerable(specifier)){context.report({node,messageId:"violationDetected"})}},ImportExpression(node){if(isSteerable(node.source)){context.report({node,messageId:"violationDetected"})}}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicRequire=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");exports.noDynamicRequire=(0,eslint_devkit_1.createRule)({name:"no-dynamic-require",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-require.md",description:"Forbid `require()` calls with expressions",cwe:"CWE-94",cweJustification:"CWE-94 (Improper Control of Generation of Code) \u2014 dynamic require with attacker-influenced path can load arbitrary modules, equivalent to remote code execution.",confidence:"high"},hasSuggestions:false,messages:{dynamicRequire:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.WARNING,issueName:"Dynamic Require",description:"Require call uses dynamic expression",severity:"HIGH",fix:"Use static string literals for require() calls",documentationLink:"https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-dynamic-require.md"})},schema:[{type:"object",properties:{allowContexts:{type:"array",items:{type:"string",enum:["test","config","build","runtime"]},description:"Allow dynamic requires in specific contexts."},allowPatterns:{type:"array",items:{type:"string"},description:"Regex patterns for allowed dynamic require paths."}},additionalProperties:false}]},defaultOptions:[{allowContexts:[],allowPatterns:[]}],create(context){const[options]=context.options;const{allowContexts=[]}=options||{};const filename=context.filename||"";function isInAllowedContext(){if(allowContexts.includes("test")&&(filename.includes(".test.")||filename.includes(".spec.")||filename.includes("/__tests__/"))){return true}if(allowContexts.includes("config")&&(filename.includes("config")||filename.includes("webpack")||filename.includes("rollup"))){return true}if(allowContexts.includes("build")&&(filename.includes("build")||filename.includes("scripts")||filename.includes("tools"))){return true}if(allowContexts.includes("runtime")&&(filename.includes("runtime")||filename.includes("dynamic"))){return true}return false}function
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicRequire=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");exports.noDynamicRequire=(0,eslint_devkit_1.createRule)({name:"no-dynamic-require",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-require.md",description:"Forbid `require()` calls with expressions",cwe:"CWE-94",cweJustification:"CWE-94 (Improper Control of Generation of Code) \u2014 dynamic require with attacker-influenced path can load arbitrary modules, equivalent to remote code execution.",confidence:"high"},hasSuggestions:false,messages:{dynamicRequire:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.WARNING,issueName:"Dynamic Require",cwe:"CWE-94",description:"Require call uses dynamic expression",severity:"HIGH",fix:"Use static string literals for require() calls",documentationLink:"https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-dynamic-require.md"})},schema:[{type:"object",properties:{allowContexts:{type:"array",items:{type:"string",enum:["test","config","build","runtime"]},description:"Allow dynamic requires in specific contexts."},allowPatterns:{type:"array",items:{type:"string"},description:"Regex patterns for allowed dynamic require paths."}},additionalProperties:false}]},defaultOptions:[{allowContexts:[],allowPatterns:[]}],create(context){const[options]=context.options;const{allowContexts=[]}=options||{};const filename=context.filename||"";function isInAllowedContext(){if(allowContexts.includes("test")&&(filename.includes(".test.")||filename.includes(".spec.")||filename.includes("/__tests__/"))){return true}if(allowContexts.includes("config")&&(filename.includes("config")||filename.includes("webpack")||filename.includes("rollup"))){return true}if(allowContexts.includes("build")&&(filename.includes("build")||filename.includes("scripts")||filename.includes("tools"))){return true}if(allowContexts.includes("runtime")&&(filename.includes("runtime")||filename.includes("dynamic"))){return true}return false}function isFixedSpecifier(node){return(0,eslint_devkit_1.isStaticExpression)({node,scope:context.sourceCode.getScope(node)})}return{CallExpression(node){if(node.callee.type==="Identifier"&&node.callee.name==="require"&&node.arguments.length===1){const requireArg=node.arguments[0];if(isInAllowedContext()){return}if(isFixedSpecifier(requireArg)){return}context.report({node:requireArg,messageId:"dynamicRequire"})}}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSelfSignedCerts=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noSelfSignedCerts=(0,eslint_devkit_1.createRule)({name:"no-self-signed-certs",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-self-signed-certs.md",description:"Disallow rejectUnauthorized: false in TLS options",cwe:"CWE-295",cvss:
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSelfSignedCerts=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noSelfSignedCerts=(0,eslint_devkit_1.createRule)({name:"no-self-signed-certs",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-self-signed-certs.md",description:"Disallow rejectUnauthorized: false in TLS options",cwe:"CWE-295",cvss:7.4},hasSuggestions:true,messages:{insecureTls:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"TLS certificate validation disabled",cwe:"CWE-295",description:"rejectUnauthorized: false disables TLS certificate validation, enabling man-in-the-middle attacks. Any certificate will be accepted, including self-signed and expired ones.",severity:"CRITICAL",fix:"Remove rejectUnauthorized: false or set it to true",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html"}),enableValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Enable certificate validation",description:"Enable proper TLS certificate validation",severity:"LOW",fix:"rejectUnauthorized: true (or remove the property)",documentationLink:"https://nodejs.org/api/tls.html#tlsconnectoptions-callback"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);function checkProperty(node){if(isTestFile)return;if(node.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.key.name==="rejectUnauthorized"&&node.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&node.value.value===false){context.report({node,messageId:"insecureTls",suggest:[{messageId:"enableValidation",fix:fixer=>{return fixer.replaceText(node.value,"true")}}]})}}function checkAssignmentExpression(node){if(isTestFile)return;if(node.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.left.object.object.name==="process"&&node.left.object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.left.object.property.name==="env"&&node.left.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.left.property.name==="NODE_TLS_REJECT_UNAUTHORIZED"){if(node.right.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&(node.right.value==="0"||node.right.value===0)){context.report({node,messageId:"insecureTls",suggest:[{messageId:"enableValidation",fix:()=>null}]})}}}return{Property:checkProperty,AssignmentExpression:checkAssignmentExpression}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_SECRET_PATTERNS=["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];function isSourceConstant(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.name==="undefined")return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return node.expressions.length===0;return false}const BOOLEAN_PREDICATE_NAME=/^(?:is|has|should|can|did|was|will|does)[A-Z]/;const NAMED_CONSTANT=/^[A-Z][A-Z0-9_]*$/;const DEFAULT_UNTRUSTED_SOURCES=["req","request","ctx","event"];const NAMESPACE_NAME=/^(?:[A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)$/;function isNamedConstant(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.computed)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return NAMESPACE_NAME.test(node.object.name)&&NAMED_CONSTANT.test(node.property.name)}function memberRoot(node){let current=node;for(;;){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object;continue}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){current=current.callee;continue}break}return current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?current:null}function isSelfComparison(left,right){const pair=(bare,derived)=>{if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(derived.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return memberRoot(derived)?.name===bare.name};return pair(left,right)||pair(right,left)}const NON_SECRET_MEMBERS=new Map([["hash",new Set(["location"])]]);function isNonSecretMember(node){if(node.computed||node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const receivers=NON_SECRET_MEMBERS.get(node.property.name.toLowerCase());if(!receivers)return false;const owner=node.object;const ownerName=owner.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.name:owner.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!owner.computed&&owner.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.property.name:"";return receivers.has(ownerName.toLowerCase())}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_SECRET_PATTERNS=["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];function isSourceConstant(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.name==="undefined")return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return node.expressions.length===0;return false}const BOOLEAN_PREDICATE_NAME=/^(?:is|has|should|can|did|was|will|does)[A-Z]/;const NAMED_CONSTANT=/^[A-Z][A-Z0-9_]*$/;const DEFAULT_UNTRUSTED_SOURCES=["req","request","ctx","event"];const NAMESPACE_NAME=/^(?:[A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)$/;function isNamedConstant(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.computed)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return NAMESPACE_NAME.test(node.object.name)&&NAMED_CONSTANT.test(node.property.name)}function memberRoot(node){let current=node;for(;;){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object;continue}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){current=current.callee;continue}break}return current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?current:null}function isSelfComparison(left,right){const pair=(bare,derived)=>{if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(derived.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return memberRoot(derived)?.name===bare.name};return pair(left,right)||pair(right,left)}const NON_SECRET_MEMBERS=new Map([["hash",new Set(["location"])]]);function isNonSecretMember(node){if(node.computed||node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const receivers=NON_SECRET_MEMBERS.get(node.property.name.toLowerCase());if(!receivers)return false;const owner=node.object;const ownerName=owner.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.name:owner.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!owner.computed&&owner.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.property.name:"";return receivers.has(ownerName.toLowerCase())}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:5.9},hasSuggestions:true,messages:{timingUnsafeCompare:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Timing-unsafe comparison",cwe:"CWE-208",description:"Using === to compare secrets enables timing attacks. The comparison short-circuits on first mismatch, leaking information about the secret.",severity:"HIGH",fix:"Use crypto.timingSafeEqual() for constant-time comparison",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"}),useTimingSafeEqual:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use timingSafeEqual",description:"Use constant-time comparison to prevent timing attacks",severity:"LOW",fix:"crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"},untrustedSources:{type:"array",items:{type:"string"},default:DEFAULT_UNTRUSTED_SOURCES,description:"Identifier roots treated as attacker-controlled (default: req, request, ctx, event)"},reportUnverifiedComparisons:{type:"boolean",default:false,description:"Report on a secret-looking name alone, without an attacker-controlled operand. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS,untrustedSources=DEFAULT_UNTRUSTED_SOURCES,reportUnverifiedComparisons=false}=options;const sourceCode=context.sourceCode;const readsUntrusted=(0,provenance_1.makeReadsTaintSource)(sourceCode,new Set(untrustedSources.map(source=>source.toLowerCase())));const patterns=secretPatterns.map(p=>new RegExp(p,"i"));function nameLooksSecret(name){if(BOOLEAN_PREDICATE_NAME.test(name))return false;return patterns.some(p=>p.test(name))}function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(node.name)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){if(isNonSecretMember(node))return false;const prop=node.property;if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(prop.name)}}return false}function isResolvedConstant(node){if(isSourceConstant(node))return true;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return(0,provenance_1.constLiteralOf)(sourceCode,node)!==void 0}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}if(isResolvedConstant(node.left)||isResolvedConstant(node.right)){return}if(isNamedConstant(node.left)||isNamedConstant(node.right)){return}if(isSelfComparison(node.left,node.right)){return}const leftIsSecret=isSecretIdentifier(node.left);const rightIsSecret=isSecretIdentifier(node.right);if(leftIsSecret||rightIsSecret){if(!reportUnverifiedComparisons){const leftUntrusted=readsUntrusted(node.left);const rightUntrusted=readsUntrusted(node.right);if(leftUntrusted===rightUntrusted)return}context.report({node,messageId:"timingUnsafeCompare",suggest:[{messageId:"useTimingSafeEqual",fix:()=>null}]})}}return{BinaryExpression:checkBinaryExpression}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noToctouVulnerability=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const PER_USER_ROOT_FUNCTIONS=new Set(["homedir","userInfo"]);const PER_USER_ENV_VARS=new Set(["HOME","USERPROFILE","LOCALAPPDATA","APPDATA","XDG_CACHE_HOME","XDG_CONFIG_HOME","XDG_DATA_HOME","XDG_STATE_HOME"]);exports.noToctouVulnerability=(0,eslint_devkit_2.createRule)({name:"no-toctou-vulnerability",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-toctou-vulnerability.md",description:"Detects Time-of-Check-Time-of-Use vulnerabilities",cwe:"CWE-367",cvss:7
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noToctouVulnerability=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const PER_USER_ROOT_FUNCTIONS=new Set(["homedir","userInfo"]);const PER_USER_ENV_VARS=new Set(["HOME","USERPROFILE","LOCALAPPDATA","APPDATA","XDG_CACHE_HOME","XDG_CONFIG_HOME","XDG_DATA_HOME","XDG_STATE_HOME"]);exports.noToctouVulnerability=(0,eslint_devkit_2.createRule)({name:"no-toctou-vulnerability",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-toctou-vulnerability.md",description:"Detects Time-of-Check-Time-of-Use vulnerabilities",cwe:"CWE-367",cvss:7},hasSuggestions:true,messages:{toctouVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"TOCTOU vulnerability",cwe:"CWE-367",description:"Time-of-check Time-of-use race condition detected",severity:"HIGH",fix:"Use atomic operations or fs.promises for file operations",documentationLink:"https://cwe.mitre.org/data/definitions/367.html"}),useAtomicOperations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Atomic Operations",description:"Use atomic file operations",severity:"LOW",fix:"fs.promises.access() then fs.promises.readFile()",documentationLink:"https://nodejs.org/api/fs.html#fspromisesaccesspath-mode"}),useFsPromises:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use fs.promises",description:"Use fs.promises API",severity:"LOW",fix:"await fs.promises.readFile() instead of sync operations",documentationLink:"https://nodejs.org/api/fs.html#promises-api"}),addProperLocking:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Add File Locking",description:"Add proper locking mechanism",severity:"LOW",fix:"Use proper-lockfile or similar for concurrent access",documentationLink:"https://github.com/moxystudio/node-proper-lockfile"})},schema:[{type:"object",properties:{ignoreInTests:{type:"boolean",default:true},fsMethods:{type:"array",items:{type:"string"},default:["fs.existsSync","fs.statSync","fs.accessSync"],description:"Filesystem check calls that create a time-of-check window"}},additionalProperties:false}]},defaultOptions:[{ignoreInTests:true,fsMethods:["fs.existsSync","fs.statSync","fs.accessSync"]}],create(context,[options={}]){const{ignoreInTests=true}=options||{};const filename=context.filename;const isTestFile=ignoreInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);if(isTestFile){return{}}const sourceCode=context.sourceCode;function returnedExpression(callee){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type!=="FunctionName")return null;const body=def.node.body;if(body?.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return null;for(const statement of body.body){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&statement.argument){return statement.argument}}return null}function reachesPerUserRoot(node,depth=0){if(depth>8)return false;const next=child=>reachesPerUserRoot(child,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const variable=(0,provenance_1.findVariable)(sourceCode,node);const def=variable?.defs.length===1?variable.defs[0]:void 0;if(def?.type!=="Variable"||!def.node.init)return false;return next(def.node.init)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if(!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&PER_USER_ENV_VARS.has(node.property.name)){return true}return next(node.object)}case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;const name=callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property.name:null;if(name!==null&&PER_USER_ROOT_FUNCTIONS.has(name))return true;if(node.arguments.some(argument=>next(argument)))return true;const returned=returnedExpression(callee);return returned!==null&&next(returned)}case eslint_devkit_1.AST_NODE_TYPES.ConditionalExpression:return next(node.consequent)||next(node.alternate);case eslint_devkit_1.AST_NODE_TYPES.LogicalExpression:return next(node.left)||next(node.right);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>next(expression));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return next(node.left)||next(node.right);default:return false}}const EXISTENCE_CHECKS=new Set(["existsSync","exists"]);const READ_ONLY_USES=new Set(["readFileSync","readFile"]);function isSecurityRelevantWindow(checkMethod,useMethod){if(!EXISTENCE_CHECKS.has(checkMethod))return true;return!READ_ONLY_USES.has(useMethod)}function checkCallExpression(node){let useMethodName="";if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const objectName=node.callee.object.type==="Identifier"?node.callee.object.name:"";if(objectName==="fs"||objectName==="fsPromises"){useMethodName=node.callee.property.name}}else if(node.callee.type==="Identifier"){useMethodName=node.callee.name}const riskyUseMethods=["readFileSync","writeFileSync","readFile","writeFile","openSync","open","unlinkSync","unlink"];if(!riskyUseMethods.includes(useMethodName)){return}const useArg=node.arguments[0];if(!useArg)return;if(reachesPerUserRoot(useArg))return;let current=node.parent;while(current){if(current.type==="IfStatement"){let condition=current.test;if(condition.type==="UnaryExpression"&&condition.operator==="!"){condition=condition.argument}if(condition.type==="CallExpression"){let checkMethodName="";if(condition.callee.type==="MemberExpression"&&condition.callee.property.type==="Identifier"){checkMethodName=condition.callee.property.name}else if(condition.callee.type==="Identifier"){checkMethodName=condition.callee.name}const checkMethods=["existsSync","statSync","accessSync","exists","stat","access"];if(checkMethods.includes(checkMethodName)&&isSecurityRelevantWindow(checkMethodName,useMethodName)){const checkArg=condition.arguments[0];if(checkArg){if(checkArg.type==="Identifier"&&useArg.type==="Identifier"&&checkArg.name===useArg.name){reportToctou(node);return}const checkArgText=sourceCode.getText(checkArg).replace(/\s/g,"");const useArgText=sourceCode.getText(useArg).replace(/\s/g,"");if(checkArgText===useArgText){reportToctou(node);return}}}if(condition.callee.type==="MemberExpression"&&condition.callee.property.type==="Identifier"&&["isFile","isDirectory"].includes(condition.callee.property.name)&&condition.callee.object.type==="Identifier"){const statsVarName=condition.callee.object.name;let currentScope=sourceCode.getScope(condition);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===statsVarName)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init&&def.node.init.type==="CallExpression"){const init=def.node.init;if(init.callee.type==="MemberExpression"&&init.callee.property.type==="Identifier"&&["statSync","lstatSync","stat","lstat"].includes(init.callee.property.name)){const statArg=init.arguments[0];if(statArg){const checkArgText=sourceCode.getText(statArg).replace(/\s/g,"");const useArgText=sourceCode.getText(useArg).replace(/\s/g,"");if(checkArgText===useArgText){reportToctou(node);return}}}}}}}}current=current.parent}}function reportToctou(node){context.report({node,messageId:"toctouVulnerability",suggest:[{messageId:"useAtomicOperations",fix:()=>null},{messageId:"useFsPromises",fix:()=>null},{messageId:"addProperLocking",fix:()=>null}]})}return{CallExpression:checkCallExpression}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireSecureCredentialStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireSecureCredentialStorage=(0,eslint_devkit_1.createRule)({name:"require-secure-credential-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-secure-credential-storage.md",description:"Enforce secure storage patterns for credentials",cwe:"CWE-312",cvss:
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireSecureCredentialStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const credential_evidence_1=require("../../utils/credential-evidence");exports.requireSecureCredentialStorage=(0,eslint_devkit_1.createRule)({name:"require-secure-credential-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-secure-credential-storage.md",description:"Enforce secure storage patterns for credentials",cwe:"CWE-312",cvss:5.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"violation Detected",cwe:"CWE-312",description:"Enforce secure storage patterns for credentials detected - Credentials without encryption",severity:"HIGH",fix:"Review and apply secure practices",documentationLink:"https://cwe.mitre.org/data/definitions/312.html"})},schema:[]},defaultOptions:[],create(context){return{CallExpression(node){if(!(0,credential_evidence_1.isWebStorageWrite)(node))return;if(!(0,credential_evidence_1.storesACredential)(node)||(0,credential_evidence_1.isEncrypted)(node))return;context.report({node,messageId:"violationDetected"})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireSecureDeletion=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const SENSITIVE_PROPERTY_FRAGMENTS=["password","passwd","pwd","passphrase","secret","apikey","api_key","token","jwt","bearer","credential","privatekey","private_key","signingkey","signing_key","sessionid","session_id","refreshtoken","refresh_token","ssn","creditcard","credit_card","cardnumber","card_number","cvv"];exports.requireSecureDeletion=(0,eslint_devkit_1.createRule)({name:"require-secure-deletion",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-secure-deletion.md",description:"Require secure data deletion patterns",cwe:"CWE-459",cvss:5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Incomplete Secret Cleanup",cwe:"CWE-459",description:"`delete` on the sensitive property `{{property}}` unbinds it without scrubbing the value",severity:"MEDIUM",fix:"Overwrite the value before deleting it (obj.{{property}} = undefined, or zero-fill the Buffer), and make sure no copy of the object was spread, logged, or serialised first",documentationLink:"https://cwe.mitre.org/data/definitions/459.html"})},schema:[{type:"object",properties:{additionalSensitiveProperties:{type:"array",items:{type:"string"},default:[],description:"Extra property-name fragments to treat as sensitive"}},additionalProperties:false}]},defaultOptions:[{additionalSensitiveProperties:[]}],create(context,[options={}]){const{additionalSensitiveProperties=[]}=options;const fragments=[...SENSITIVE_PROPERTY_FRAGMENTS,...additionalSensitiveProperties.map(f=>f.toLowerCase())];function deletedPropertyName(node){const argument=node.type==="ChainExpression"?node.expression:node;if(argument.type!=="MemberExpression")return void 0;const property=argument.property;if(!argument.computed&&property.type==="Identifier")return property.name;if(argument.computed&&property.type==="Literal"&&typeof property.value==="string"){return property.value}return void 0}return{UnaryExpression(node){if(node.operator!=="delete")return;const property=deletedPropertyName(node.argument);if(!property)return;const normalized=property.toLowerCase().replace(/[^a-z0-9_]/g,"");if(!fragments.some(fragment=>normalized.includes(fragment)))return;context.report({node,messageId:"violationDetected",data:{property}})}}}});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireSecureDeletion=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const SENSITIVE_PROPERTY_FRAGMENTS=["password","passwd","pwd","passphrase","secret","apikey","api_key","token","jwt","bearer","credential","privatekey","private_key","signingkey","signing_key","sessionid","session_id","refreshtoken","refresh_token","ssn","creditcard","credit_card","cardnumber","card_number","cvv"];exports.requireSecureDeletion=(0,eslint_devkit_1.createRule)({name:"require-secure-deletion",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-secure-deletion.md",description:"Require secure data deletion patterns",cwe:"CWE-459",cvss:5.3},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Incomplete Secret Cleanup",cwe:"CWE-459",description:"`delete` on the sensitive property `{{property}}` unbinds it without scrubbing the value",severity:"MEDIUM",fix:"Overwrite the value before deleting it (obj.{{property}} = undefined, or zero-fill the Buffer), and make sure no copy of the object was spread, logged, or serialised first",documentationLink:"https://cwe.mitre.org/data/definitions/459.html"})},schema:[{type:"object",properties:{additionalSensitiveProperties:{type:"array",items:{type:"string"},default:[],description:"Extra property-name fragments to treat as sensitive"}},additionalProperties:false}]},defaultOptions:[{additionalSensitiveProperties:[]}],create(context,[options={}]){const{additionalSensitiveProperties=[]}=options;const fragments=[...SENSITIVE_PROPERTY_FRAGMENTS,...additionalSensitiveProperties.map(f=>f.toLowerCase())];function deletedPropertyName(node){const argument=node.type==="ChainExpression"?node.expression:node;if(argument.type!=="MemberExpression")return void 0;const property=argument.property;if(!argument.computed&&property.type==="Identifier")return property.name;if(argument.computed&&property.type==="Literal"&&typeof property.value==="string"){return property.value}return void 0}return{UnaryExpression(node){if(node.operator!=="delete")return;const property=deletedPropertyName(node.argument);if(!property)return;const normalized=property.toLowerCase().replace(/[^a-z0-9_]/g,"");if(!fragments.some(fragment=>normalized.includes(fragment)))return;context.report({node,messageId:"violationDetected",data:{property}})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireStorageEncryption=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireStorageEncryption=(0,eslint_devkit_1.createRule)({name:"require-storage-encryption",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-storage-encryption.md",description:"Require encryption for persistent storage",cwe:"CWE-312",cvss:
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireStorageEncryption=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const credential_evidence_1=require("../../utils/credential-evidence");exports.requireStorageEncryption=(0,eslint_devkit_1.createRule)({name:"require-storage-encryption",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-storage-encryption.md",description:"Require encryption for persistent storage",cwe:"CWE-312",cvss:5.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"violation Detected",cwe:"CWE-312",description:"Require encryption for persistent storage detected - Storage without encryption",severity:"HIGH",fix:"Review and apply secure practices",documentationLink:"https://cwe.mitre.org/data/definitions/312.html"})},schema:[]},defaultOptions:[],create(context){return{CallExpression(node){if(!(0,credential_evidence_1.isFileWrite)(node))return;if(!(0,credential_evidence_1.storesACredential)(node)||(0,credential_evidence_1.isEncrypted)(node))return;context.report({node,messageId:"violationDetected"})}}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.storesACredential=storesACredential;exports.isEncrypted=isEncrypted;exports.isWebStorageWrite=isWebStorageWrite;exports.isFileWrite=isFileWrite;const eslint_devkit_1=require("@interlace/eslint-devkit");const CREDENTIAL_WORDS=["password","passwd","secret","token","credential","apikey","api_key","accesskey","access_key","privatekey","private_key","jwt","sessionid","session_id","refreshtoken","refresh_token","clientsecret","client_secret","authtoken","auth_token"];function namesACredential(text){const normalized=text.toLowerCase();return CREDENTIAL_WORDS.some(word=>normalized.includes(word))}function nameOf(node){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:return node.name;case eslint_devkit_1.AST_NODE_TYPES.Literal:return typeof node.value==="string"?node.value:"";case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.quasis.map(q=>q.value.raw).join("");case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node.property.name:"";default:return""}}function storesACredential(node){return node.arguments.some(argument=>{if(argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return false;return namesACredential(nameOf(argument))})}function calleeEncrypts(name){return name.split(/(?=[A-Z])|_/).some(token=>token.toLowerCase().startsWith("encrypt"))}function isEncrypted(node){const value=node.arguments[1];if(value?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const callee=value.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return calleeEncrypts(callee.name)}return callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&calleeEncrypts(callee.property.name)}const CLIENT_STORES=new Set(["localStorage","sessionStorage","AsyncStorage"]);function isWebStorageWrite(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(callee.property.name!=="setItem")return false;const object=callee.object;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return CLIENT_STORES.has(object.name)}return object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&CLIENT_STORES.has(object.property.name)}function isFileWrite(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return["writeFile","writeFileSync","appendFile","appendFileSync"].includes(callee.property.name)}
|