eslint-plugin-node-security 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- 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-eval-with-expression/index.js +1 -1
- package/src/rules/detect-non-literal-fs-filename/index.js +1 -1
- package/src/rules/no-arbitrary-file-access/index.js +1 -1
- package/src/rules/no-buffer-overread/index.js +1 -1
- package/src/rules/no-data-in-temp-storage/index.js +1 -1
- package/src/rules/no-deprecated-buffer/index.js +1 -1
- package/src/rules/no-deprecated-cipher-method/index.js +1 -1
- package/src/rules/no-dynamic-algorithm-selection/index.js +1 -1
- package/src/rules/no-dynamic-command-string/index.js +1 -1
- package/src/rules/no-dynamic-dependency-loading/index.js +1 -1
- package/src/rules/no-ecb-mode/index.js +1 -1
- package/src/rules/no-env-injection/index.js +1 -1
- package/src/rules/no-insecure-key-derivation/index.js +1 -1
- package/src/rules/no-insecure-rsa-padding/index.js +1 -1
- package/src/rules/no-math-random-crypto/index.js +1 -1
- package/src/rules/no-self-signed-certs/index.js +1 -1
- package/src/rules/no-shell-injection/index.js +1 -1
- package/src/rules/no-ssrf/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/no-unbounded-decompression/index.js +1 -1
- package/src/rules/no-unsafe-buffer-alloc/index.js +1 -1
- package/src/rules/no-unsafe-dynamic-require/index.js +1 -1
- package/src/rules/no-weak-cipher-algorithm/index.js +1 -1
- package/src/rules/no-weak-hash-algorithm/index.js +1 -1
- package/src/rules/no-zip-slip/index.js +1 -1
- package/src/rules/require-aead-tag-verification/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-stream-error-handler/index.js +1 -1
- package/src/utils/credential-evidence.js +1 -1
- package/src/utils/provenance.js +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<p align="center">
|
|
16
16
|
<a href="https://www.npmjs.com/package/eslint-plugin-node-security" target="_blank"><img src="https://img.shields.io/npm/v/eslint-plugin-node-security.svg" alt="NPM Version" /></a>
|
|
17
17
|
<a href="https://www.npmjs.com/package/eslint-plugin-node-security" target="_blank"><img src="https://img.shields.io/npm/dm/eslint-plugin-node-security.svg" alt="NPM Downloads" /></a>
|
|
18
|
+
<a href="https://packagephobia.com/result?p=eslint-plugin-node-security" target="_blank"><img src="https://badgen.net/packagephobia/install/eslint-plugin-node-security" alt="Install Size" /></a>
|
|
18
19
|
<a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="Package License" /></a>
|
|
19
20
|
<a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=eslint-plugin-node-security" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=eslint-plugin-node-security" alt="Codecov" /></a>
|
|
20
21
|
<a href="https://github.com/ofri-peretz/eslint" target="_blank"><img src="https://img.shields.io/badge/Since-Dec_2025-blue?logo=rocket&logoColor=white" alt="Since Dec 2025" /></a>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-node-security",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
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.19.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:"5.
|
|
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:"5.4.0"},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_INVOCATIONS=new Map([["node",new Set(["-e","--eval","-p","--print"])],["nodejs",new Set(["-e","--eval","-p","--print"])],["bun",new Set(["-e","--eval","-p","--print"])],["deno",new Set(["eval"])],["python",new Set(["-c"])],["python2",new Set(["-c"])],["python3",new Set(["-c"])],["ruby",new Set(["-e"])],["perl",new Set(["-e","-E"])],["php",new Set(["-r"])],["osascript",new Set(["-e"])]]);const NO_EVAL_TOKENS=new Set;const EVAL_FLAGS=new Set(["-c","-e","--eval","-e:","/c","/k","-command","-encodedcommand","-p","--print","-e","-r","eval"]);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"},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"}),untrustedProgram:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Untrusted program",cwe:"CWE-114",description:"The executable name is attacker-steerable. No shell runs here, so metacharacters are inert \u2014 but the caller still chooses which program executes.",severity:"HIGH",fix:"Resolve the name against a fixed allowlist of permitted executables before spawning it.",documentationLink:"https://cwe.mitre.org/data/definitions/114.html"}),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"})},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"},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:[]}],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 namedBinary=command!==void 0&&command.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof command.value==="string"?command.value.replace(/^.*[/\\]/,"").toLowerCase():null;const evalTokens=namedBinary===null?EVAL_FLAGS:EVAL_INVOCATIONS.get(namedBinary)??NO_EVAL_TOKENS;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"&&evalTokens.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"});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:usesShell(node,method)?"childProcessCommandInjection":"untrustedProgram",data:{method,args,riskLevel,vulnerability:pattern?.vulnerability||"command injection",alternatives,steps,effort:pattern?.effort||"15-30 minutes"}})};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_INVOCATIONS=new Map([["node",new Set(["-e","--eval","-p","--print"])],["nodejs",new Set(["-e","--eval","-p","--print"])],["bun",new Set(["-e","--eval","-p","--print"])],["deno",new Set(["eval"])],["python",new Set(["-c"])],["python2",new Set(["-c"])],["python3",new Set(["-c"])],["ruby",new Set(["-e"])],["perl",new Set(["-e","-E"])],["php",new Set(["-r"])],["osascript",new Set(["-e"])]]);const NO_EVAL_TOKENS=new Set;const EVAL_FLAGS=new Set(["-c","-e","--eval","-e:","/c","/k","-command","-encodedcommand","-p","--print","-e","-r","eval"]);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"},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"}),untrustedProgram:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Untrusted program",cwe:"CWE-114",description:"The executable name is attacker-steerable. No shell runs here, so metacharacters are inert \u2014 but the caller still chooses which program executes.",severity:"HIGH",fix:"Resolve the name against a fixed allowlist of permitted executables before spawning it.",documentationLink:"https://cwe.mitre.org/data/definitions/114.html"}),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"})},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"},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:[]}],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 namedBinary=command!==void 0&&command.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof command.value==="string"?command.value.replace(/^.*[/\\]/,"").toLowerCase():null;const evalTokens=namedBinary===null?EVAL_FLAGS:EVAL_INVOCATIONS.get(namedBinary)??NO_EVAL_TOKENS;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"&&evalTokens.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"&&(0,eslint_devkit_1.propertyName)(test.callee)==="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&&(0,eslint_devkit_1.propertyName)(test.argument.callee)==="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"){const methodName=(0,eslint_devkit_1.propertyName)(node.callee);if(methodName===null||!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"});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:usesShell(node,method)?"childProcessCommandInjection":"untrustedProgram",data:{method,args,riskLevel,vulnerability:pattern?.vulnerability||"command injection",alternatives,steps,effort:pattern?.effort||"15-30 minutes"}})};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.detectEvalWithExpression=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const EVAL_PATTERNS=[{pattern:"JSON\\.parse|parse\\(.*\\)",category:"json",safeAlternative:"JSON.parse()",example:{bad:`eval('{"key": "' + value + '"}"')`,good:`JSON.parse('{"key": "' + value + '"}"')`},effort:"2 minutes"},{pattern:"Math\\.|parseInt|parseFloat",category:"math",safeAlternative:"Math functions or parseInt/parseFloat",example:{bad:"eval('Math.' + method + '(' + arg + ')')",good:"const mathMethods = {sin: Math.sin, cos: Math.cos}; mathMethods[method](arg)"},effort:"5 minutes"},{pattern:"\\$\\{|template|interpolat",category:"template",safeAlternative:"Template literals or template engine",example:{bad:"eval('Hello ' + userName + '!')",good:"const template = `Hello ${userName}!`;"},effort:"3 minutes"},{pattern:"\\[.*\\]|object\\[|obj\\.|\\.",category:"object",safeAlternative:"Direct property access or Map",example:{bad:"eval('obj.' + property)",good:"const allowedProps = {name: true, age: true}; if (allowedProps[property]) obj[property]"},effort:"8 minutes"}];const VM_CODE_SINK_METHODS=new Set(["runInNewContext","runInThisContext","runInContext","compileFunction"]);const VM_CODE_CONSTRUCTORS=new Set(["Script","SourceTextModule"]);const VM2_SANDBOX_CONSTRUCTORS=new Set(["VM","NodeVM"]);const VM2_CODE_CONSTRUCTORS=new Set(["VMScript"]);const VM_MODULES=new Set(["vm","node:vm"]);const VM2_MODULES=new Set(["vm2"]);const NAMESPACE="*";function requiredModule(node){if(!node||node.type!=="CallExpression")return null;if(node.callee.type!=="Identifier"||node.callee.name!=="require"){return null}const[source]=node.arguments;if(!source||source.type!=="Literal")return null;return typeof source.value==="string"?source.value:null}function resolveModuleMember(callee,bindings,modules,name){if(name===null)return null;if(callee.type==="Identifier"){const bound=bindings.get(name);return bound!==void 0&&bound!==NAMESPACE?bound:null}const{object}=callee;if(object.type==="Identifier"){return bindings.get(object.name)===NAMESPACE?name:null}const inline=requiredModule(object);return inline!==null&&modules.has(inline)?name:null}function calleeTrailingName(callee){if(callee.type==="Identifier")return callee.name;if(callee.type!=="MemberExpression"||callee.computed)return null;return callee.property.type==="Identifier"?callee.property.name:null}function isStaticStringNode(node){if(node.type==="Literal")return(0,eslint_devkit_1.staticString)(node)!==null;if(node.type==="TemplateLiteral")return node.expressions.length===0;return false}const generateRefactoringSteps=pattern=>{if(!pattern){return[" 1. Remove eval() usage entirely"," 2. Identify what the code is trying to achieve"," 3. Use appropriate safe alternative (JSON.parse, Map, etc.)"," 4. Add input validation if dynamic behavior needed"," 5. Test thoroughly for edge cases"].join("\n")}switch(pattern.category){case"json":return[" 1. Replace eval() with JSON.parse()"," 2. Ensure input is valid JSON string"," 3. Add try/catch for JSON parsing errors"," 4. Consider using a JSON schema validator"].join("\n");case"math":return[" 1. Create whitelist of allowed Math functions"," 2. Use direct function calls: Math.sin(x)"," 3. Validate inputs are numbers"," 4. Consider using a math expression parser library"].join("\n");case"template":return[" 1. Use template literals: `Hello ${name}`"," 2. Sanitize variables before interpolation"," 3. Use a template engine like Handlebars if complex"," 4. Validate template structure"].join("\n");case"object":return[" 1. Use Map or plain object for key-value access"," 2. Whitelist allowed property names"," 3. Use hasOwnProperty() check"," 4. Consider Object.create(null) for clean objects"].join("\n");default:return[" 1. Identify the specific use case"," 2. Find a safer alternative approach"," 3. Add comprehensive input validation"," 4. Use static analysis if possible"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectEvalWithExpression=(0,eslint_devkit_2.createRule)({name:"detect-eval-with-expression",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-eval-with-expression.md",description:"Detects strings turned into running code \u2014 eval(variable), the Function constructor, and the vm / vm2 sinks that are mistaken for sandboxes",cwe:"CWE-95",cvss:9.8,confidence:"high"},hasSuggestions:false,messages:{vmCodeExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Code Execution Through the vm Module (CWE-94)",cwe:"CWE-94",cvss:9.8,description:'vm.{{api}}() compiles and runs its first argument as JavaScript, and that argument is not written out in full here. The vm module is NOT a security boundary \u2014 Node documents it as such \u2014 because any object reachable from the context carries a constructor chain back out: `this.constructor.constructor("return process")()`. A string that is not a constant is a string an attacker may be able to steer.',severity:"CRITICAL",fix:"Do not evaluate the value as code. Parse it (JSON.parse, an expression parser) or dispatch through a fixed map of allowed operations; if untrusted code genuinely has to run, isolate it in a separate process with its own privileges \u2014 not in vm.",documentationLink:"https://nodejs.org/api/vm.html#vm-executing-javascript"}),vm2CodeExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Code Execution Through vm2 (CWE-94)",cwe:"CWE-94",cvss:9.8,description:"vm2 is abandoned and was retired by its maintainer after sandbox escapes that it could not fix (CVE-2023-37903, CVE-2023-37466). Running source that is not a constant inside it is arbitrary code execution on the host, not sandboxed execution.",severity:"CRITICAL",fix:"Stop using vm2. Run untrusted code out-of-process under an OS-level boundary (a separate process with dropped privileges, a container, or isolated-vm), or remove the need to execute caller-supplied source.",documentationLink:"https://github.com/patriksimek/vm2/issues/533"}),useJsonParse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for JSON parsing",cwe:"CWE-95",description:"Use JSON.parse() instead of eval() for JSON string parsing",severity:"HIGH",fix:"Replace eval() with JSON.parse()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useObjectAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for property access",cwe:"CWE-95",description:"Use direct property access instead of eval() for dynamic property access",severity:"HIGH",fix:"Use obj[key] or Map.get(key) instead of eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useTemplateLiteral:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for string interpolation",cwe:"CWE-95",description:"Use template literals instead of eval() for string interpolation",severity:"HIGH",fix:"Replace eval() with template literals: `Hello ${name}`",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRemove:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Critical eval() security vulnerability",cwe:"CWE-95",description:"eval() usage poses severe security risk",severity:"CRITICAL",fix:"Remove eval() entirely - security risk too high",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRefactor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() refactoring required",cwe:"CWE-95",description:"eval() can be refactored to safer alternative",severity:"HIGH",fix:"{{safeAlternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() input validation needed",cwe:"CWE-95",description:"eval() requires input validation for security",severity:"MEDIUM",fix:"Add input validation before using eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow eval with literal strings (false = stricter)"},additionalEvalFunctions:{type:"array",items:{type:"string"},default:[],description:"Additional functions to treat as eval-like"},strategy:{type:"string",enum:["remove","refactor","validate","auto"],default:"auto",description:"Strategy for fixing eval usage (auto = smart detection)"}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,additionalEvalFunctions:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,additionalEvalFunctions=[],strategy="auto"}=options;const evalFunctions=new Set(["eval","Function",...additionalEvalFunctions]);const isLiteralString=node=>{return(0,eslint_devkit_1.staticString)(node)!==null};const selectStrategyMessage=pattern=>{switch(strategy){case"remove":return"strategyRemove";case"refactor":return"strategyRefactor";case"validate":return"strategyValidate";case"auto":default:if(pattern&&pattern.category==="json"){return"useJsonParse"}if(pattern&&pattern.category==="object"){return"useObjectAccess"}if(pattern&&pattern.category==="template"){return"useTemplateLiteral"}return"strategyRefactor"}};const detectPattern=expression=>{for(const pattern of EVAL_PATTERNS){if(new RegExp(pattern.pattern,"i").test(expression)){return pattern}}return null};const extractExpression=node=>{const sourceCode=context.sourceCode;if(node.arguments.length>0){return sourceCode.getText(node.arguments[0])}return"dynamic expression"};const GLOBAL_OBJECTS=new Set(["globalThis","global","window","self"]);const isGlobalBinding=identifier=>{const variable=findVariable(identifier);return!variable||variable.defs.length===0};const evalCalleeName=(callee,depth=0)=>{if(depth>3)return null;if(callee.type==="Identifier"){if(evalFunctions.has(callee.name)){if(isGlobalBinding(callee))return callee.name;const aliased=(0,const_value_1.constInitializerOf)(context.sourceCode,callee);return aliased?evalCalleeName(aliased,depth+1):null}const init=(0,const_value_1.constInitializerOf)(context.sourceCode,callee);return init?evalCalleeName(init,depth+1):null}if(callee.type==="SequenceExpression"){const[last]=callee.expressions.slice(-1);return evalCalleeName(last,depth+1)}if(callee.type==="MemberExpression"&&!callee.computed&&callee.object.type==="Identifier"&&GLOBAL_OBJECTS.has(callee.object.name)&&callee.property.type==="Identifier"&&evalFunctions.has(callee.property.name)){return callee.property.name}return null};const checkCallExpression=node=>{const evalName=evalCalleeName(node.callee);if(evalName!==null){if(allowLiteralStrings&&node.arguments.length>0&&isLiteralString(node.arguments[0])){return}if(node.arguments.length>0&&evalName==="eval"&&isLiteralString(node.arguments[0])){return}const expression=extractExpression(node);const pattern=detectPattern(expression);const steps=(0,exports.generateRefactoringSteps)(pattern);context.report({node,messageId:selectStrategyMessage(pattern),data:{expression,patternCategory:pattern?.category||"dynamic code execution",safeAlternative:pattern?.safeAlternative||"Remove eval entirely",steps,effort:pattern?.effort||"15-30 minutes"}})}};const checkNewExpression=node=>{if(node.callee.type==="Identifier"&&node.callee.name==="Function"&&isGlobalBinding(node.callee)){const sourceCode=context.sourceCode;const expression=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=detectPattern(expression);context.report({node,messageId:selectStrategyMessage(pattern),data:{expression:`new Function(${expression})`,patternCategory:"function constructor",safeAlternative:"Arrow function or regular function",steps:[" 1. Replace Function constructor with arrow function"," 2. Use regular function declaration"," 3. Validate any dynamic parts"," 4. Consider module imports instead"].join("\n"),effort:"10 minutes"}})}};const vmBindings=new Map;const vm2Bindings=new Map;const vm2SandboxCandidates=[];const pendingVmCalls=[];const pendingVmNews=[];const findVariable=node=>{let scope=context.sourceCode.getScope(node);while(scope){const found=scope.variables.find(v=>v.name===node.name);if(found)return found;scope=scope.upper}return null};const isStaticCode=node=>{if(!node)return true;if(isStaticStringNode(node))return true;if(node.type!=="Identifier")return false;const variable=findVariable(node);if(!variable)return false;const lastWrite=variable.references.filter(ref=>ref.isWrite()).map(ref=>ref.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]).at(-1);return lastWrite!==void 0&&isStaticStringNode(lastWrite)};const bindModuleName=(moduleName,local,imported)=>{if(VM_MODULES.has(moduleName))vmBindings.set(local,imported);else if(VM2_MODULES.has(moduleName))vm2Bindings.set(local,imported)};const bindRequire=node=>{const moduleName=requiredModule(node.init);if(moduleName===null)return;if(node.id.type==="Identifier"){bindModuleName(moduleName,node.id.name,NAMESPACE);return}if(node.id.type!=="ObjectPattern")return;for(const property of node.id.properties){if(property.type!=="Property")continue;if(property.key.type!=="Identifier")continue;if(property.value.type!=="Identifier")continue;bindModuleName(moduleName,property.value.name,property.key.name)}};const reportVmSite=(node,code,messageId,api)=>{if(isStaticCode(code))return;context.report({node,messageId,data:{api}})};const trailingName=callee=>{if(callee.type==="MemberExpression"&&callee.computed){return(0,const_value_1.resolveConstantString)(context.sourceCode,callee.property)?.value??null}return calleeTrailingName(callee)};const isVm2Run=(node,sandboxes)=>{const{callee}=node;if(callee.type!=="MemberExpression")return false;if(trailingName(callee)!=="run")return false;if(callee.object.type==="Identifier"){return sandboxes.has(callee.object.name)}if(callee.object.type!=="NewExpression")return false;const constructed=resolveModuleMember(callee.object.callee,vm2Bindings,VM2_MODULES,trailingName(callee.object.callee));return constructed!==null&&VM2_SANDBOX_CONSTRUCTORS.has(constructed)};const judgeVmSites=()=>{const sandboxes=new Set;for(const candidate of vm2SandboxCandidates){const constructed=resolveModuleMember(candidate.init.callee,vm2Bindings,VM2_MODULES,trailingName(candidate.init.callee));if(constructed!==null&&VM2_SANDBOX_CONSTRUCTORS.has(constructed)){sandboxes.add(candidate.local)}}for(const node of pendingVmCalls){const name=trailingName(node.callee);const vmApi=resolveModuleMember(node.callee,vmBindings,VM_MODULES,name);if(vmApi!==null&&VM_CODE_SINK_METHODS.has(vmApi)){reportVmSite(node,node.arguments[0],"vmCodeExecution",vmApi);continue}if(isVm2Run(node,sandboxes)){reportVmSite(node,node.arguments[0],"vm2CodeExecution","run")}}for(const node of pendingVmNews){const name=trailingName(node.callee);const vmCtor=resolveModuleMember(node.callee,vmBindings,VM_MODULES,name);if(vmCtor!==null&&VM_CODE_CONSTRUCTORS.has(vmCtor)){reportVmSite(node,node.arguments[0],"vmCodeExecution",vmCtor);continue}const vm2Ctor=resolveModuleMember(node.callee,vm2Bindings,VM2_MODULES,name);if(vm2Ctor!==null&&VM2_CODE_CONSTRUCTORS.has(vm2Ctor)){reportVmSite(node,node.arguments[0],"vm2CodeExecution",vm2Ctor)}}};return{CallExpression(node){checkCallExpression(node);if(node.callee.type==="Identifier"){pendingVmCalls.push(node);return}const name=trailingName(node.callee);if(name!==null&&(VM_CODE_SINK_METHODS.has(name)||name==="run")){pendingVmCalls.push(node)}},NewExpression(node){checkNewExpression(node);if(node.callee.type==="Identifier"){pendingVmNews.push(node);return}const name=trailingName(node.callee);if(name!==null&&(VM_CODE_CONSTRUCTORS.has(name)||VM2_CODE_CONSTRUCTORS.has(name))){pendingVmNews.push(node)}},ImportDeclaration(node){const moduleName=node.source.value;for(const specifier of node.specifiers){if(specifier.type==="ImportSpecifier"){if(specifier.imported.type!=="Identifier")continue;bindModuleName(moduleName,specifier.local.name,specifier.imported.name);continue}bindModuleName(moduleName,specifier.local.name,NAMESPACE)}},VariableDeclarator(node){bindRequire(node);if(node.id.type==="Identifier"&&node.init?.type==="NewExpression"){vm2SandboxCandidates.push({local:node.id.name,init:node.init})}},"Program:exit":judgeVmSites}}});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectEvalWithExpression=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const EVAL_PATTERNS=[{pattern:"JSON\\.parse|parse\\(.*\\)",category:"json",safeAlternative:"JSON.parse()",example:{bad:`eval('{"key": "' + value + '"}"')`,good:`JSON.parse('{"key": "' + value + '"}"')`},effort:"2 minutes"},{pattern:"Math\\.|parseInt|parseFloat",category:"math",safeAlternative:"Math functions or parseInt/parseFloat",example:{bad:"eval('Math.' + method + '(' + arg + ')')",good:"const mathMethods = {sin: Math.sin, cos: Math.cos}; mathMethods[method](arg)"},effort:"5 minutes"},{pattern:"\\$\\{|template|interpolat",category:"template",safeAlternative:"Template literals or template engine",example:{bad:"eval('Hello ' + userName + '!')",good:"const template = `Hello ${userName}!`;"},effort:"3 minutes"},{pattern:"\\[.*\\]|object\\[|obj\\.|\\.",category:"object",safeAlternative:"Direct property access or Map",example:{bad:"eval('obj.' + property)",good:"const allowedProps = {name: true, age: true}; if (allowedProps[property]) obj[property]"},effort:"8 minutes"}];const VM_CODE_SINK_METHODS=new Set(["runInNewContext","runInThisContext","runInContext","compileFunction"]);const VM_CODE_CONSTRUCTORS=new Set(["Script","SourceTextModule"]);const VM2_SANDBOX_CONSTRUCTORS=new Set(["VM","NodeVM"]);const VM2_CODE_CONSTRUCTORS=new Set(["VMScript"]);const VM_MODULES=new Set(["vm","node:vm"]);const VM2_MODULES=new Set(["vm2"]);const NAMESPACE="*";function requiredModule(node){if(!node||node.type!=="CallExpression")return null;if(node.callee.type!=="Identifier"||node.callee.name!=="require"){return null}const[source]=node.arguments;if(!source||source.type!=="Literal")return null;return typeof source.value==="string"?source.value:null}function resolveModuleMember(callee,bindings,modules,name){if(name===null)return null;if(callee.type==="Identifier"){const bound=bindings.get(name);return bound!==void 0&&bound!==NAMESPACE?bound:null}const{object}=callee;if(object.type==="Identifier"){return bindings.get(object.name)===NAMESPACE?name:null}const inline=requiredModule(object);return inline!==null&&modules.has(inline)?name:null}function calleeTrailingName(callee){if(callee.type==="Identifier")return callee.name;if(callee.type!=="MemberExpression"||callee.computed)return null;return callee.property.type==="Identifier"?callee.property.name:null}function isStaticStringNode(node){if(node.type==="Literal")return(0,eslint_devkit_1.staticString)(node)!==null;if(node.type==="TemplateLiteral")return node.expressions.length===0;return false}const generateRefactoringSteps=pattern=>{if(!pattern){return[" 1. Remove eval() usage entirely"," 2. Identify what the code is trying to achieve"," 3. Use appropriate safe alternative (JSON.parse, Map, etc.)"," 4. Add input validation if dynamic behavior needed"," 5. Test thoroughly for edge cases"].join("\n")}switch(pattern.category){case"json":return[" 1. Replace eval() with JSON.parse()"," 2. Ensure input is valid JSON string"," 3. Add try/catch for JSON parsing errors"," 4. Consider using a JSON schema validator"].join("\n");case"math":return[" 1. Create whitelist of allowed Math functions"," 2. Use direct function calls: Math.sin(x)"," 3. Validate inputs are numbers"," 4. Consider using a math expression parser library"].join("\n");case"template":return[" 1. Use template literals: `Hello ${name}`"," 2. Sanitize variables before interpolation"," 3. Use a template engine like Handlebars if complex"," 4. Validate template structure"].join("\n");case"object":return[" 1. Use Map or plain object for key-value access"," 2. Whitelist allowed property names"," 3. Use hasOwnProperty() check"," 4. Consider Object.create(null) for clean objects"].join("\n");default:return[" 1. Identify the specific use case"," 2. Find a safer alternative approach"," 3. Add comprehensive input validation"," 4. Use static analysis if possible"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectEvalWithExpression=(0,eslint_devkit_2.createRule)({name:"detect-eval-with-expression",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-eval-with-expression.md",description:"Detects strings turned into running code \u2014 eval(variable), the Function constructor, and the vm / vm2 sinks that are mistaken for sandboxes",cwe:"CWE-95",cvss:9.8,confidence:"high"},hasSuggestions:false,messages:{vmCodeExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Code Execution Through the vm Module (CWE-94)",cwe:"CWE-94",cvss:9.8,description:'vm.{{api}}() compiles and runs its first argument as JavaScript, and that argument is not written out in full here. The vm module is NOT a security boundary \u2014 Node documents it as such \u2014 because any object reachable from the context carries a constructor chain back out: `this.constructor.constructor("return process")()`. A string that is not a constant is a string an attacker may be able to steer.',severity:"CRITICAL",fix:"Do not evaluate the value as code. Parse it (JSON.parse, an expression parser) or dispatch through a fixed map of allowed operations; if untrusted code genuinely has to run, isolate it in a separate process with its own privileges \u2014 not in vm.",documentationLink:"https://nodejs.org/api/vm.html#vm-executing-javascript"}),vm2CodeExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Code Execution Through vm2 (CWE-94)",cwe:"CWE-94",cvss:9.8,description:"vm2 is abandoned and was retired by its maintainer after sandbox escapes that it could not fix (CVE-2023-37903, CVE-2023-37466). Running source that is not a constant inside it is arbitrary code execution on the host, not sandboxed execution.",severity:"CRITICAL",fix:"Stop using vm2. Run untrusted code out-of-process under an OS-level boundary (a separate process with dropped privileges, a container, or isolated-vm), or remove the need to execute caller-supplied source.",documentationLink:"https://github.com/patriksimek/vm2/issues/533"}),useJsonParse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for JSON parsing",cwe:"CWE-95",description:"Use JSON.parse() instead of eval() for JSON string parsing",severity:"HIGH",fix:"Replace eval() with JSON.parse()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useObjectAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for property access",cwe:"CWE-95",description:"Use direct property access instead of eval() for dynamic property access",severity:"HIGH",fix:"Use obj[key] or Map.get(key) instead of eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useTemplateLiteral:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for string interpolation",cwe:"CWE-95",description:"Use template literals instead of eval() for string interpolation",severity:"HIGH",fix:"Replace eval() with template literals: `Hello ${name}`",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRemove:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Critical eval() security vulnerability",cwe:"CWE-95",description:"eval() usage poses severe security risk",severity:"CRITICAL",fix:"Remove eval() entirely - security risk too high",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRefactor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() refactoring required",cwe:"CWE-95",description:"eval() can be refactored to safer alternative",severity:"HIGH",fix:"{{safeAlternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() input validation needed",cwe:"CWE-95",description:"eval() requires input validation for security",severity:"MEDIUM",fix:"Add input validation before using eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow eval with literal strings (false = stricter)"},additionalEvalFunctions:{type:"array",items:{type:"string"},default:[],description:"Additional functions to treat as eval-like"},strategy:{type:"string",enum:["remove","refactor","validate","auto"],default:"auto",description:"Strategy for fixing eval usage (auto = smart detection)"}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,additionalEvalFunctions:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,additionalEvalFunctions=[],strategy="auto"}=options;const evalFunctions=new Set(["eval","Function",...additionalEvalFunctions]);const isLiteralString=node=>{return(0,eslint_devkit_1.staticString)(node)!==null};const selectStrategyMessage=pattern=>{switch(strategy){case"remove":return"strategyRemove";case"refactor":return"strategyRefactor";case"validate":return"strategyValidate";case"auto":default:if(pattern&&pattern.category==="json"){return"useJsonParse"}if(pattern&&pattern.category==="object"){return"useObjectAccess"}if(pattern&&pattern.category==="template"){return"useTemplateLiteral"}return"strategyRefactor"}};const detectPattern=expression=>{for(const pattern of EVAL_PATTERNS){if(new RegExp(pattern.pattern,"i").test(expression)){return pattern}}return null};const extractExpression=node=>{const sourceCode=context.sourceCode;if(node.arguments.length>0){return sourceCode.getText(node.arguments[0])}return"dynamic expression"};const GLOBAL_OBJECTS=new Set(["globalThis","global","window","self"]);const isGlobalBinding=identifier=>{const variable=findVariable(identifier);return!variable||variable.defs.length===0};const evalCalleeName=(callee,depth=0)=>{if(depth>3)return null;if(callee.type==="Identifier"){if(evalFunctions.has(callee.name)){if(isGlobalBinding(callee))return callee.name;const aliased=(0,const_value_1.constInitializerOf)(context.sourceCode,callee);return aliased?evalCalleeName(aliased,depth+1):null}const init=(0,const_value_1.constInitializerOf)(context.sourceCode,callee);return init?evalCalleeName(init,depth+1):null}if(callee.type==="SequenceExpression"){const[last]=callee.expressions.slice(-1);return evalCalleeName(last,depth+1)}if(callee.type==="MemberExpression"&&callee.object.type==="Identifier"&&GLOBAL_OBJECTS.has(callee.object.name)&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),evalFunctions)){return(0,eslint_devkit_1.propertyName)(callee)}return null};const checkCallExpression=node=>{const evalName=evalCalleeName(node.callee);if(evalName!==null){if(allowLiteralStrings&&node.arguments.length>0&&isLiteralString(node.arguments[0])){return}if(node.arguments.length>0&&evalName==="eval"&&isLiteralString(node.arguments[0])){return}const expression=extractExpression(node);const pattern=detectPattern(expression);const steps=(0,exports.generateRefactoringSteps)(pattern);context.report({node,messageId:selectStrategyMessage(pattern),data:{expression,patternCategory:pattern?.category||"dynamic code execution",safeAlternative:pattern?.safeAlternative||"Remove eval entirely",steps,effort:pattern?.effort||"15-30 minutes"}})}};const checkNewExpression=node=>{if(node.callee.type==="Identifier"&&node.callee.name==="Function"&&isGlobalBinding(node.callee)){const sourceCode=context.sourceCode;const expression=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=detectPattern(expression);context.report({node,messageId:selectStrategyMessage(pattern),data:{expression:`new Function(${expression})`,patternCategory:"function constructor",safeAlternative:"Arrow function or regular function",steps:[" 1. Replace Function constructor with arrow function"," 2. Use regular function declaration"," 3. Validate any dynamic parts"," 4. Consider module imports instead"].join("\n"),effort:"10 minutes"}})}};const vmBindings=new Map;const vm2Bindings=new Map;const vm2SandboxCandidates=[];const pendingVmCalls=[];const pendingVmNews=[];const findVariable=node=>{let scope=context.sourceCode.getScope(node);while(scope){const found=scope.variables.find(v=>v.name===node.name);if(found)return found;scope=scope.upper}return null};const isStaticCode=node=>{if(!node)return true;if(isStaticStringNode(node))return true;if(node.type!=="Identifier")return false;const variable=findVariable(node);if(!variable)return false;const lastWrite=variable.references.filter(ref=>ref.isWrite()).map(ref=>ref.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]).at(-1);return lastWrite!==void 0&&isStaticStringNode(lastWrite)};const bindModuleName=(moduleName,local,imported)=>{if(VM_MODULES.has(moduleName))vmBindings.set(local,imported);else if(VM2_MODULES.has(moduleName))vm2Bindings.set(local,imported)};const bindRequire=node=>{const moduleName=requiredModule(node.init);if(moduleName===null)return;if(node.id.type==="Identifier"){bindModuleName(moduleName,node.id.name,NAMESPACE);return}if(node.id.type!=="ObjectPattern")return;for(const property of node.id.properties){if(property.type!=="Property")continue;if(property.key.type!=="Identifier")continue;if(property.value.type!=="Identifier")continue;bindModuleName(moduleName,property.value.name,property.key.name)}};const reportVmSite=(node,code,messageId,api)=>{if(isStaticCode(code))return;context.report({node,messageId,data:{api}})};const trailingName=callee=>{if(callee.type==="MemberExpression"&&callee.computed){return(0,const_value_1.resolveConstantString)(context.sourceCode,callee.property)?.value??null}return calleeTrailingName(callee)};const isVm2Run=(node,sandboxes)=>{const{callee}=node;if(callee.type!=="MemberExpression")return false;if(trailingName(callee)!=="run")return false;if(callee.object.type==="Identifier"){return sandboxes.has(callee.object.name)}if(callee.object.type!=="NewExpression")return false;const constructed=resolveModuleMember(callee.object.callee,vm2Bindings,VM2_MODULES,trailingName(callee.object.callee));return constructed!==null&&VM2_SANDBOX_CONSTRUCTORS.has(constructed)};const judgeVmSites=()=>{const sandboxes=new Set;for(const candidate of vm2SandboxCandidates){const constructed=resolveModuleMember(candidate.init.callee,vm2Bindings,VM2_MODULES,trailingName(candidate.init.callee));if(constructed!==null&&VM2_SANDBOX_CONSTRUCTORS.has(constructed)){sandboxes.add(candidate.local)}}for(const node of pendingVmCalls){const name=trailingName(node.callee);const vmApi=resolveModuleMember(node.callee,vmBindings,VM_MODULES,name);if(vmApi!==null&&VM_CODE_SINK_METHODS.has(vmApi)){reportVmSite(node,node.arguments[0],"vmCodeExecution",vmApi);continue}if(isVm2Run(node,sandboxes)){reportVmSite(node,node.arguments[0],"vm2CodeExecution","run")}}for(const node of pendingVmNews){const name=trailingName(node.callee);const vmCtor=resolveModuleMember(node.callee,vmBindings,VM_MODULES,name);if(vmCtor!==null&&VM_CODE_CONSTRUCTORS.has(vmCtor)){reportVmSite(node,node.arguments[0],"vmCodeExecution",vmCtor);continue}const vm2Ctor=resolveModuleMember(node.callee,vm2Bindings,VM2_MODULES,name);if(vm2Ctor!==null&&VM2_CODE_CONSTRUCTORS.has(vm2Ctor)){reportVmSite(node,node.arguments[0],"vm2CodeExecution",vm2Ctor)}}};return{CallExpression(node){checkCallExpression(node);if(node.callee.type==="Identifier"){pendingVmCalls.push(node);return}const name=trailingName(node.callee);if(name!==null&&(VM_CODE_SINK_METHODS.has(name)||name==="run")){pendingVmCalls.push(node)}},NewExpression(node){checkNewExpression(node);if(node.callee.type==="Identifier"){pendingVmNews.push(node);return}const name=trailingName(node.callee);if(name!==null&&(VM_CODE_CONSTRUCTORS.has(name)||VM2_CODE_CONSTRUCTORS.has(name))){pendingVmNews.push(node)}},ImportDeclaration(node){const moduleName=node.source.value;for(const specifier of node.specifiers){if(specifier.type==="ImportSpecifier"){if(specifier.imported.type!=="Identifier")continue;bindModuleName(moduleName,specifier.local.name,specifier.imported.name);continue}bindModuleName(moduleName,specifier.local.name,NAMESPACE)}},VariableDeclarator(node){bindRequire(node);if(node.id.type==="Identifier"&&node.init?.type==="NewExpression"){vm2SandboxCandidates.push({local:node.id.name,init:node.init})}},"Program:exit":judgeVmSites}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectNonLiteralFsFilename=exports.determineRiskLevel=exports.isFsModule=exports.generateRefactoringSteps=void 0;exports.fsMethodName=fsMethodName;exports.isFsRequire=isFsRequire;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const FS_OPERATIONS=[{method:"readFile",dangerous:true,vulnerability:"file-access",safePattern:"path.resolve(SAFE_DIR, path.basename(userInput))",example:{bad:"fs.readFile(userPath, callback)",good:"const safePath = path.join(SAFE_UPLOADS_DIR, path.basename(userPath)); fs.readFile(safePath, callback)"},effort:"10-15 minutes"},{method:"writeFile",dangerous:true,vulnerability:"file-access",safePattern:"path.resolve(SAFE_DIR, path.basename(userInput))",example:{bad:"fs.writeFile(userPath, data, callback)",good:"const safePath = path.join(SAFE_WRITES_DIR, path.basename(userPath)); fs.writeFile(safePath, data, callback)"},effort:"10-15 minutes"},{method:"stat",dangerous:true,vulnerability:"path-traversal",safePattern:"path.resolve(baseDir, userInput) with validation",example:{bad:"fs.stat(userPath, callback)",good:"const resolvedPath = path.resolve(SAFE_DIR, userPath);\nif (!resolvedPath.startsWith(SAFE_DIR + path.sep)) return;\nfs.stat(resolvedPath, callback)"},effort:"15-20 minutes"},{method:"readdir",dangerous:true,vulnerability:"directory-traversal",safePattern:"Validate directory is within allowed paths",example:{bad:"fs.readdir(userDir, callback)",good:"const resolvedDir = path.resolve(ALLOWED_DIRS, userDir);\nif (!resolvedDir.startsWith(ALLOWED_DIRS + path.sep)) return;\nfs.readdir(resolvedDir, callback)"},effort:"15-20 minutes"}];const hasTraversalPatterns=pathStr=>{return/\.\.[/\\]/.test(pathStr)||/^\.\.[/\\]/.test(pathStr)};const PROCESS_INPUT_MEMBERS=new Set(["env","argv","argv0","execArgv","stdin"]);const SENSITIVE_SEGMENTS=["etc/passwd","etc/shadow","etc/hosts","etc/sudoers","proc/self",".ssh/id_rsa",".ssh/id_dsa",".ssh/authorized_keys",".aws/credentials",".npmrc",".git/config","windows/system32/config/sam"];const targetsSensitiveLocation=pathStr=>{const unquoted=pathStr.replace(/^['"`]|['"`]$/g,"");const normalised=unquoted.replace(/\\/g,"/").toLowerCase();if(!/(^|\/)\.\.(\/|$)/.test(normalised)){return false}const withoutPrefix=normalised.replace(/^(?:\.{1,2}\/)+/,"");return SENSITIVE_SEGMENTS.some(seg=>withoutPrefix===seg||withoutPrefix.endsWith(`/${seg}`)||withoutPrefix.startsWith(`${seg}/`))};const generateRefactoringSteps=operation=>{switch(operation.method){case"readFile":case"writeFile":return[" 1. Define a SAFE_DIR constant for allowed operations"," 2. Use path.basename() to strip directory components"," 3. Combine with SAFE_DIR: path.join(SAFE_DIR, path.basename(userPath))"," 4. Optionally validate file extensions"," 5. Add error handling for invalid paths"].join("\n");case"stat":return[" 1. Use path.resolve() to normalize the path"," 2. Check the resolved path starts with the base PLUS path.sep \u2014 a bare"," prefix lets /safebad through a /safe check"," 3. Reject requests that escape the allowed directory"," 4. Use path.relative() for additional validation"," 5. Log security events for monitoring"].join("\n");case"readdir":return[" 1. Resolve the directory path: path.resolve(ALLOWED_DIRS, userDir)"," 2. Validate resolved path starts with ALLOWED_DIRS"," 3. Check directory exists and is readable"," 4. Consider whitelisting allowed directories"," 5. Add rate limiting to prevent enumeration attacks"].join("\n");default:return[" 1. Identify the specific file operation needed"," 2. Define safe base directories for operations"," 3. Use path.resolve() and validate containment"," 4. Sanitize user input (basename, extension validation)"," 5. Add comprehensive error handling"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;const FS_MODULE_EQUIVALENTS={"fs-extra":"fs","graceful-fs":"fs","fs/promises":"fs"};const FS_MODULES=new Set(["fs","node:fs","fs/promises","node:fs/promises","fs-extra","graceful-fs"]);const isFsModule=source=>typeof source==="string"&&FS_MODULES.has(source);exports.isFsModule=isFsModule;function fsMethodName(callee,namespaces,named){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return named.get(callee.name);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return void 0}const object=callee.object;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.name)){return callee.property.name}if(object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!object.computed&&object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.object.name)&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.property.name==="promises"){return callee.property.name}return void 0}function isFsRequire(node){return 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.length>0&&node.arguments[0].type===eslint_devkit_1.AST_NODE_TYPES.Literal&&(0,exports.isFsModule)(node.arguments[0].value)}const determineRiskLevel=(operation,pathStr)=>{if(hasTraversalPatterns(pathStr)){return"CRITICAL"}if(operation.dangerous){return"HIGH"}return"MEDIUM"};exports.determineRiskLevel=determineRiskLevel;exports.detectNonLiteralFsFilename=(0,eslint_devkit_2.createRule)({name:"detect-non-literal-fs-filename",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-non-literal-fs-filename.md",description:"Detects variable in filename argument of fs calls, which might allow an attacker to access anything on your system",cwe:"CWE-22",confidence:"medium"},messages:{fsPathTraversal:(0,eslint_devkit_1.formatLLMMessage)({icon:"\u{1F511}",issueName:"Path traversal",cwe:"CWE-22",description:"Path traversal vulnerability",severity:"{{riskLevel}}",fix:"{{safePattern}} \u2014 Not a finding when the value is path.basename()d, checked against an allowlist, or resolved and then prefix-checked WITH a trailing separator",documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"})},schema:[{type:"object",properties:{taintSources:{type:"array",items:{type:"string"},default:["process"],description:"Identifier roots treated as attacker-reachable. Default: ['process'] (process.argv and process.env). Request roots (req/request/ctx/event) are deliberately NOT included \u2014 no-arbitrary-file-access owns those, and listing them here would double-report one line at two severities. Add them only if you run this rule without that one."},reportUnresolvedPaths:{type:"boolean",default:false,description:"Report paths whose provenance cannot be resolved. Restores the pre-inversion behaviour; measured at 7% precision on real code."},allowLiterals:{type:"boolean",default:false,description:`Allow literal string paths. Default true: a rule named "non-literal" reporting a literal contradicts its contract. Set false to also flag hardcoded paths containing "../" \u2014 measured as this rule's largest FP class on real code.`},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional fs methods to check"}},additionalProperties:false}]},skipTestFiles:true,defaultOptions:[{allowLiterals:false,additionalMethods:[]}],create(context){const options=context.options[0]||{};const{allowLiterals=false,additionalMethods=[]}=options;const DEFAULT_TAINT_ROOTS=["process","req","request","ctx","context","event"];const WHOLE_VALUE_TRUSTED_ROOTS=new Set(["process"]);const AMBIGUOUS_ROOTS=new Set(["ctx","context"]);const REQUEST_SURFACE=new Set(["query","params","body","request","req","headers","cookies","searchParams","originalUrl"]);const resolvesInFile=id=>context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id)?.resolved!=null;const readsRequestSurface=node=>node?.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&REQUEST_SURFACE.has(node.property.name);const hasRequestEvidence=id=>{if(readsRequestSurface(id.parent))return true;const variable=context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id).resolved;return variable.references.some(ref=>readsRequestSurface(ref.identifier.parent))};const taintRoots=new Set(options.taintSources??DEFAULT_TAINT_ROOTS);const reportUnresolvedPaths=options.reportUnresolvedPaths??false;const PATH_ARGUMENT_INDICES=new Map([["copyFile",[0,1]],["copyFileSync",[0,1]],["cp",[0,1]],["cpSync",[0,1]],["rename",[0,1]],["renameSync",[0,1]],["link",[0,1]],["linkSync",[0,1]],["symlink",[0,1]],["symlinkSync",[0,1]]]);const dangerousMethods=new Set(["readFile","readFileSync","writeFile","writeFileSync","appendFile","appendFileSync","stat","statSync","lstat","lstatSync","readdir","readdirSync","unlink","unlinkSync","mkdir","mkdirSync","rmdir","rmdirSync","access","accessSync","createReadStream","createWriteStream","open","openSync","rm","rmSync","rename","renameSync","copyFile","copyFileSync","cp","cpSync","truncate","truncateSync","chmod","chmodSync","chown","chownSync","lchown","lchownSync","utimes","utimesSync","readlink","readlinkSync","symlink","symlinkSync","link","linkSync","opendir","opendirSync",...additionalMethods]);const isLiteralString=node=>{return(0,eslint_devkit_1.staticString)(node)!==null};const isLocallyConstructed=id=>{const variable=context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id)?.resolved;if(!variable||variable.defs.length!==1)return false;const def=variable.defs[0];if(def.type!=="Variable")return false;if(variable.references.filter(ref=>ref.isWrite()).length>1){return false}const init=def.node.init;return init?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression||init?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression};const readsTaintSource=(node,depth=0)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name)){if(AMBIGUOUS_ROOTS.has(node.name)&&resolvesInFile(node)&&!hasRequestEvidence(node)){return false}return!isLocallyConstructed(node)}const bound=constBindings.get(node.name);return bound!==void 0&&readsTaintSource(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if(!node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.object.name==="process"&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!PROCESS_INPUT_MEMBERS.has(node.property.name)){return false}return readsTaintSource(node.object,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>readsTaintSource(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return readsTaintSource(node.left,depth+1)||readsTaintSource(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.name==="basename"){return false}return node.arguments.some(arg=>arg.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsTaintSource(arg,depth+1))}default:return false}};const containsUntrustedRoot=(node,depth=0)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name))return!WHOLE_VALUE_TRUSTED_ROOTS.has(node.name);const bound=constBindings.get(node.name);return bound!==void 0&&containsUntrustedRoot(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return containsUntrustedRoot(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>containsUntrustedRoot(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return containsUntrustedRoot(node.left,depth+1)||containsUntrustedRoot(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return node.arguments.some(a=>a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&containsUntrustedRoot(a,depth+1));default:return false}};const isWholeTaintValue=node=>{switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name))return WHOLE_VALUE_TRUSTED_ROOTS.has(node.name);const bound=constBindings.get(node.name);return bound!==void 0&&isWholeTaintValue(bound)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isWholeTaintValue(node.object);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;const isPathCall=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"||(0,eslint_devkit_1.resolveModuleBinding)(callee,context.sourceCode.getScope(callee),{})?.module==="path";if(!isPathCall)return false;if(node.arguments.length===1)return isWholeTaintValue(node.arguments[0]);return isWholeTaintValue(node.arguments[0])&&!containsUntrustedRoot(node)}default:return false}};const isDangerousPath=(pathNode,pathStr)=>{if(!pathNode)return reportUnresolvedPaths;if(isLiteralString(pathNode)){return!allowLiterals&&targetsSensitiveLocation(pathStr)}if(hasPathValidation(pathNode))return false;if(readsTaintSource(pathNode))return!isWholeTaintValue(pathNode);if(isBuildTimeConstant(pathNode))return false;if(isFreeVariable(pathNode))return true;if(containsFreeVariable(pathNode))return true;return reportUnresolvedPaths};function isFreeVariable(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const through=context.sourceCode.getScope(node).through;return through.some(ref=>ref.identifier===node&&ref.resolved===null)}const containsFreeVariable=(node,depth=0)=>{if(depth>4)return false;if(isBuildTimeConstant(node,depth))return false;if(isFreeVariable(node))return true;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>containsFreeVariable(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return node.arguments.some(a=>a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&containsFreeVariable(a,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return node.operator==="+"&&(containsFreeVariable(node.left,depth+1)||containsFreeVariable(node.right,depth+1));case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return node.computed&&containsFreeVariable(node.property,depth+1);default:return false}};const isBuildTimeConstant=(node,depth=0)=>{if(depth>4)return false;if(isLiteralString(node)){return!hasTraversalPatterns(node.value)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(node.name==="__dirname"||node.name==="__filename")return true;const bound=constBindings.get(node.name);return bound!==void 0&&isBuildTimeConstant(bound,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const literalText=node.quasis.map(q=>q.value.raw).join("");if(hasTraversalPatterns(literalText))return false;return node.expressions.every(e=>isBuildTimeConstant(e,depth+1))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="process"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.name==="cwd"){return true}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["join","resolve"].includes(callee.property.name)){return node.arguments.length>0&&node.arguments.every(arg=>isBuildTimeConstant(arg,depth+1))}return false}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"){return isBuildTimeConstant(node.left,depth+1)&&isBuildTimeConstant(node.right,depth+1)}return false};const isSeparatorAnchored=arg=>{if(arg===void 0)return false;const endsWithSep=n=>{if(n.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!n.computed&&n.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&n.object.name==="path"&&n.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&n.property.name==="sep"){return true}const staticText=(0,eslint_devkit_1.staticString)(n);if(staticText!==null){return staticText.endsWith("/")||staticText.endsWith("\\")}return false};if(endsWithSep(arg))return true;if(arg.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&arg.operator==="+"){return endsWithSep(arg.right)}if(arg.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const last=arg.quasis[arg.quasis.length-1].value.cooked;if(last.endsWith("/")||last.endsWith("\\"))return true;if(last===""){const tail=arg.expressions[arg.expressions.length-1];return tail!==void 0&&endsWithSep(tail)}return false}return false};const hasPathValidation=pathNode=>{if(pathNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){const parts=[];const collect=n=>{if(n.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&n.operator==="+"){collect(n.left);collect(n.right);return}if(n.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){n.expressions.forEach(e=>collect(e));return}if(n.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){n.arguments.forEach(a=>{if(a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement)collect(a)});return}if(readsTaintSource(n))parts.push(n)};collect(pathNode);return parts.length>0&&parts.every(p=>p.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&hasPathValidation(p))}const varName=pathNode.name;const isValidationCall=testNode=>{if(testNode.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&testNode.operator==="!"&&testNode.argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){testNode=testNode.argument}if(testNode.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression){return false}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.object.name===varName&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(testNode.callee.property.name==="startsWith"||testNode.callee.property.name==="includes")){return testNode.callee.property.name==="includes"?true:isSeparatorAnchored(testNode.arguments[0])}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.property.name==="includes"){for(const arg of testNode.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name===varName){return true}}}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.property.name==="test"){for(const arg of testNode.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name===varName){return true}}}return false};const hasEarlyExit=consequent=>{if(consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){return consequent.body.some(stmt=>stmt.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||stmt.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement)}return consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement};let current=pathNode.parent;let foundFunctionBody=false;while(current&&!foundFunctionBody){if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){if(isValidationCall(current.test)){return true}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&¤t.parent&&(current.parent.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.parent.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.parent.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression)){foundFunctionBody=true;const blockBody=current.body;const nodeIndex=blockBody.findIndex(stmt=>{let check=pathNode;while(check){if(check===stmt)return true;check=check.parent}return false});for(let i=0;i<nodeIndex;i++){const stmt=blockBody[i];if(stmt.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&isValidationCall(stmt.test)&&hasEarlyExit(stmt.consequent)){return true}}}current=current.parent}return false};const fsNamespaces=new Set(["fs"]);const fsNamedMethods=new Map;const pendingCalls=[];const constBindings=new Map;function bindFsName(local,imported){if(imported==="promises")fsNamespaces.add(local);else fsNamedMethods.set(local,imported)}const checkFsCall=node=>{let methodName=fsMethodName(node.callee,fsNamespaces,fsNamedMethods);if(methodName===void 0){const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,context.sourceCode.getScope(node),{equivalents:FS_MODULE_EQUIVALENTS});if(binding?.module!=="fs")return;const[first,second]=binding.path;methodName=binding.path.length===1?first:binding.path.length===2&&first==="promises"?second:void 0;if(methodName===void 0)return}if(!dangerousMethods.has(methodName)){return}const method=methodName;const indices=PATH_ARGUMENT_INDICES.get(method)??[0];const sourceCode=context.sourceCode;let pathNode=null;let path="";for(const index of indices){const candidate=node.arguments[index];if(candidate===void 0||candidate.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)continue;if(isDangerousPath(candidate,sourceCode.getText(candidate))){pathNode=candidate;path=sourceCode.getText(candidate);break}}if(pathNode===null){const present=indices.some(index=>node.arguments[index]!==void 0);if(!present&&isDangerousPath(null,"")){context.report({node,messageId:"fsPathTraversal",data:{method,path:"",riskLevel:"MEDIUM",vulnerability:"path traversal",safePattern:"Use path.resolve() with validation",steps:"Review file system access patterns"}})}return}const operation=FS_OPERATIONS.find(op=>op.method===method)??null;const riskLevel=(0,exports.determineRiskLevel)(operation||FS_OPERATIONS[0],path);const steps=operation?(0,exports.generateRefactoringSteps)(operation):"Review file system access patterns";const safePattern=operation?.safePattern||"Use path.resolve() with validation";context.report({node,messageId:"fsPathTraversal",data:{method,path,riskLevel,vulnerability:operation?.vulnerability||"path traversal",safePattern,steps,effort:operation?.effort||"15-20 minutes"}})};return{ImportDeclaration(node){if(!(0,exports.isFsModule)(node.source.value))return;for(const spec of node.specifiers){if(spec.type===eslint_devkit_1.AST_NODE_TYPES.ImportSpecifier){const imported=spec.imported.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?spec.imported.name:spec.imported.value;bindFsName(spec.local.name,imported);continue}fsNamespaces.add(spec.local.name)}},VariableDeclarator(node){if(node.init!==null&&node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration&&node.parent.kind==="const"){constBindings.set(node.id.name,node.init)}if(node.init===null||!isFsRequire(node.init))return;if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){fsNamespaces.add(node.id.name);return}if(node.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return;for(const prop of node.id.properties){if(prop.type!==eslint_devkit_1.AST_NODE_TYPES.Property||prop.computed)continue;if(prop.value.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)continue;const key=prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?prop.key.name:(0,eslint_devkit_1.staticString)(prop.key)??void 0;if(key===void 0)continue;bindFsName(prop.value.name,key)}},CallExpression(node){pendingCalls.push(node)},"Program:exit"(){for(const call of pendingCalls)checkFsCall(call)}}}});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectNonLiteralFsFilename=exports.determineRiskLevel=exports.isFsModule=exports.generateRefactoringSteps=void 0;exports.fsMethodName=fsMethodName;exports.isFsRequire=isFsRequire;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const FS_OPERATIONS=[{method:"readFile",dangerous:true,vulnerability:"file-access",safePattern:"path.resolve(SAFE_DIR, path.basename(userInput))",example:{bad:"fs.readFile(userPath, callback)",good:"const safePath = path.join(SAFE_UPLOADS_DIR, path.basename(userPath)); fs.readFile(safePath, callback)"},effort:"10-15 minutes"},{method:"writeFile",dangerous:true,vulnerability:"file-access",safePattern:"path.resolve(SAFE_DIR, path.basename(userInput))",example:{bad:"fs.writeFile(userPath, data, callback)",good:"const safePath = path.join(SAFE_WRITES_DIR, path.basename(userPath)); fs.writeFile(safePath, data, callback)"},effort:"10-15 minutes"},{method:"stat",dangerous:true,vulnerability:"path-traversal",safePattern:"path.resolve(baseDir, userInput) with validation",example:{bad:"fs.stat(userPath, callback)",good:"const resolvedPath = path.resolve(SAFE_DIR, userPath);\nif (!resolvedPath.startsWith(SAFE_DIR + path.sep)) return;\nfs.stat(resolvedPath, callback)"},effort:"15-20 minutes"},{method:"readdir",dangerous:true,vulnerability:"directory-traversal",safePattern:"Validate directory is within allowed paths",example:{bad:"fs.readdir(userDir, callback)",good:"const resolvedDir = path.resolve(ALLOWED_DIRS, userDir);\nif (!resolvedDir.startsWith(ALLOWED_DIRS + path.sep)) return;\nfs.readdir(resolvedDir, callback)"},effort:"15-20 minutes"}];const hasTraversalPatterns=pathStr=>{return/\.\.[/\\]/.test(pathStr)||/^\.\.[/\\]/.test(pathStr)};const PROCESS_INPUT_MEMBERS=new Set(["env","argv","argv0","execArgv","stdin"]);const SENSITIVE_SEGMENTS=["etc/passwd","etc/shadow","etc/hosts","etc/sudoers","proc/self",".ssh/id_rsa",".ssh/id_dsa",".ssh/authorized_keys",".aws/credentials",".npmrc",".git/config","windows/system32/config/sam"];const targetsSensitiveLocation=pathStr=>{const unquoted=pathStr.replace(/^['"`]|['"`]$/g,"");const normalised=unquoted.replace(/\\/g,"/").toLowerCase();if(!/(^|\/)\.\.(\/|$)/.test(normalised)){return false}const withoutPrefix=normalised.replace(/^(?:\.{1,2}\/)+/,"");return SENSITIVE_SEGMENTS.some(seg=>withoutPrefix===seg||withoutPrefix.endsWith(`/${seg}`)||withoutPrefix.startsWith(`${seg}/`))};const generateRefactoringSteps=operation=>{switch(operation.method){case"readFile":case"writeFile":return[" 1. Define a SAFE_DIR constant for allowed operations"," 2. Use path.basename() to strip directory components"," 3. Combine with SAFE_DIR: path.join(SAFE_DIR, path.basename(userPath))"," 4. Optionally validate file extensions"," 5. Add error handling for invalid paths"].join("\n");case"stat":return[" 1. Use path.resolve() to normalize the path"," 2. Check the resolved path starts with the base PLUS path.sep \u2014 a bare"," prefix lets /safebad through a /safe check"," 3. Reject requests that escape the allowed directory"," 4. Use path.relative() for additional validation"," 5. Log security events for monitoring"].join("\n");case"readdir":return[" 1. Resolve the directory path: path.resolve(ALLOWED_DIRS, userDir)"," 2. Validate resolved path starts with ALLOWED_DIRS"," 3. Check directory exists and is readable"," 4. Consider whitelisting allowed directories"," 5. Add rate limiting to prevent enumeration attacks"].join("\n");default:return[" 1. Identify the specific file operation needed"," 2. Define safe base directories for operations"," 3. Use path.resolve() and validate containment"," 4. Sanitize user input (basename, extension validation)"," 5. Add comprehensive error handling"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;const FS_MODULE_EQUIVALENTS={"fs-extra":"fs","graceful-fs":"fs","fs/promises":"fs"};const FS_MODULES=new Set(["fs","node:fs","fs/promises","node:fs/promises","fs-extra","graceful-fs"]);const isFsModule=source=>typeof source==="string"&&FS_MODULES.has(source);exports.isFsModule=isFsModule;function fsMethodName(callee,namespaces,named){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return named.get(callee.name);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression){return void 0}const method=(0,eslint_devkit_1.propertyName)(callee);if(method===null){return void 0}const object=callee.object;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.name)){return method}if(object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.object.name)&&(0,eslint_devkit_1.propertyName)(object)==="promises"){return method}return void 0}function isFsRequire(node){return 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.length>0&&node.arguments[0].type===eslint_devkit_1.AST_NODE_TYPES.Literal&&(0,exports.isFsModule)(node.arguments[0].value)}const determineRiskLevel=(operation,pathStr)=>{if(hasTraversalPatterns(pathStr)){return"CRITICAL"}if(operation.dangerous){return"HIGH"}return"MEDIUM"};exports.determineRiskLevel=determineRiskLevel;exports.detectNonLiteralFsFilename=(0,eslint_devkit_2.createRule)({name:"detect-non-literal-fs-filename",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-non-literal-fs-filename.md",description:"Detects variable in filename argument of fs calls, which might allow an attacker to access anything on your system",cwe:"CWE-22",confidence:"medium"},messages:{fsPathTraversal:(0,eslint_devkit_1.formatLLMMessage)({icon:"\u{1F511}",issueName:"Path traversal",cwe:"CWE-22",description:"Path traversal vulnerability",severity:"{{riskLevel}}",fix:"{{safePattern}} \u2014 Not a finding when the value is path.basename()d, checked against an allowlist, or resolved and then prefix-checked WITH a trailing separator",documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"})},schema:[{type:"object",properties:{taintSources:{type:"array",items:{type:"string"},default:["process"],description:"Identifier roots treated as attacker-reachable. Default: ['process'] (process.argv and process.env). Request roots (req/request/ctx/event) are deliberately NOT included \u2014 no-arbitrary-file-access owns those, and listing them here would double-report one line at two severities. Add them only if you run this rule without that one."},reportUnresolvedPaths:{type:"boolean",default:false,description:"Report paths whose provenance cannot be resolved. Restores the pre-inversion behaviour; measured at 7% precision on real code."},allowLiterals:{type:"boolean",default:false,description:`Allow literal string paths. Default true: a rule named "non-literal" reporting a literal contradicts its contract. Set false to also flag hardcoded paths containing "../" \u2014 measured as this rule's largest FP class on real code.`},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional fs methods to check"}},additionalProperties:false}]},skipTestFiles:true,defaultOptions:[{allowLiterals:false,additionalMethods:[]}],create(context){const options=context.options[0]||{};const{allowLiterals=false,additionalMethods=[]}=options;const DEFAULT_TAINT_ROOTS=["process","req","request","ctx","context","event"];const WHOLE_VALUE_TRUSTED_ROOTS=new Set(["process"]);const AMBIGUOUS_ROOTS=new Set(["ctx","context"]);const REQUEST_SURFACE=new Set(["query","params","body","request","req","headers","cookies","searchParams","originalUrl"]);const resolvesInFile=id=>context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id)?.resolved!=null;const readsRequestSurface=node=>node?.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node),REQUEST_SURFACE);const hasRequestEvidence=id=>{if(readsRequestSurface(id.parent))return true;const variable=context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id).resolved;return variable.references.some(ref=>readsRequestSurface(ref.identifier.parent))};const taintRoots=new Set(options.taintSources??DEFAULT_TAINT_ROOTS);const reportUnresolvedPaths=options.reportUnresolvedPaths??false;const PATH_ARGUMENT_INDICES=new Map([["copyFile",[0,1]],["copyFileSync",[0,1]],["cp",[0,1]],["cpSync",[0,1]],["rename",[0,1]],["renameSync",[0,1]],["link",[0,1]],["linkSync",[0,1]],["symlink",[0,1]],["symlinkSync",[0,1]]]);const dangerousMethods=new Set(["readFile","readFileSync","writeFile","writeFileSync","appendFile","appendFileSync","stat","statSync","lstat","lstatSync","readdir","readdirSync","unlink","unlinkSync","mkdir","mkdirSync","rmdir","rmdirSync","access","accessSync","createReadStream","createWriteStream","open","openSync","rm","rmSync","rename","renameSync","copyFile","copyFileSync","cp","cpSync","truncate","truncateSync","chmod","chmodSync","chown","chownSync","lchown","lchownSync","utimes","utimesSync","readlink","readlinkSync","symlink","symlinkSync","link","linkSync","opendir","opendirSync",...additionalMethods]);const isLiteralString=node=>{return(0,eslint_devkit_1.staticString)(node)!==null};const isLocallyConstructed=id=>{const variable=context.sourceCode.getScope(id).references.find(ref=>ref.identifier===id)?.resolved;if(!variable||variable.defs.length!==1)return false;const def=variable.defs[0];if(def.type!=="Variable")return false;if(variable.references.filter(ref=>ref.isWrite()).length>1){return false}const init=def.node.init;return init?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression||init?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression};const readsTaintSource=(node,depth=0)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name)){if(AMBIGUOUS_ROOTS.has(node.name)&&resolvesInFile(node)&&!hasRequestEvidence(node)){return false}return!isLocallyConstructed(node)}const bound=constBindings.get(node.name);return bound!==void 0&&readsTaintSource(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if(node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.object.name==="process"&&!(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node),PROCESS_INPUT_MEMBERS)){return false}return readsTaintSource(node.object,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>readsTaintSource(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return readsTaintSource(node.left,depth+1)||readsTaintSource(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&(0,eslint_devkit_1.propertyName)(callee)==="basename"){return false}return node.arguments.some(arg=>arg.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsTaintSource(arg,depth+1))}default:return false}};const containsUntrustedRoot=(node,depth=0)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name))return!WHOLE_VALUE_TRUSTED_ROOTS.has(node.name);const bound=constBindings.get(node.name);return bound!==void 0&&containsUntrustedRoot(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return containsUntrustedRoot(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>containsUntrustedRoot(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return containsUntrustedRoot(node.left,depth+1)||containsUntrustedRoot(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return node.arguments.some(a=>a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&containsUntrustedRoot(a,depth+1));default:return false}};const isWholeTaintValue=node=>{switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(taintRoots.has(node.name))return WHOLE_VALUE_TRUSTED_ROOTS.has(node.name);const bound=constBindings.get(node.name);return bound!==void 0&&isWholeTaintValue(bound)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isWholeTaintValue(node.object);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;const isPathCall=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"||(0,eslint_devkit_1.resolveModuleBinding)(callee,context.sourceCode.getScope(callee),{})?.module==="path";if(!isPathCall)return false;if(node.arguments.length===1)return isWholeTaintValue(node.arguments[0]);return isWholeTaintValue(node.arguments[0])&&!containsUntrustedRoot(node)}default:return false}};const isDangerousPath=(pathNode,pathStr)=>{if(!pathNode)return reportUnresolvedPaths;if(isLiteralString(pathNode)){return!allowLiterals&&targetsSensitiveLocation(pathStr)}if(hasPathValidation(pathNode))return false;if(readsTaintSource(pathNode))return!isWholeTaintValue(pathNode);if(isBuildTimeConstant(pathNode))return false;if(isFreeVariable(pathNode))return true;if(containsFreeVariable(pathNode))return true;return reportUnresolvedPaths};function isFreeVariable(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const through=context.sourceCode.getScope(node).through;return through.some(ref=>ref.identifier===node&&ref.resolved===null)}const containsFreeVariable=(node,depth=0)=>{if(depth>4)return false;if(isBuildTimeConstant(node,depth))return false;if(isFreeVariable(node))return true;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(e=>containsFreeVariable(e,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return node.arguments.some(a=>a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&containsFreeVariable(a,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return node.operator==="+"&&(containsFreeVariable(node.left,depth+1)||containsFreeVariable(node.right,depth+1));case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return node.computed&&containsFreeVariable(node.property,depth+1);default:return false}};const isBuildTimeConstant=(node,depth=0)=>{if(depth>4)return false;if(isLiteralString(node)){return!hasTraversalPatterns(node.value)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(node.name==="__dirname"||node.name==="__filename")return true;const bound=constBindings.get(node.name);return bound!==void 0&&isBuildTimeConstant(bound,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const literalText=node.quasis.map(q=>q.value.raw).join("");if(hasTraversalPatterns(literalText))return false;return node.expressions.every(e=>isBuildTimeConstant(e,depth+1))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="process"&&(0,eslint_devkit_1.propertyName)(callee)==="cwd"){return true}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["join","resolve"].includes(callee.property.name)){return node.arguments.length>0&&node.arguments.every(arg=>isBuildTimeConstant(arg,depth+1))}return false}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"){return isBuildTimeConstant(node.left,depth+1)&&isBuildTimeConstant(node.right,depth+1)}return false};const isSeparatorAnchored=arg=>{if(arg===void 0)return false;const endsWithSep=n=>{if(n.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&n.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&n.object.name==="path"&&(0,eslint_devkit_1.propertyName)(n)==="sep"){return true}const staticText=(0,eslint_devkit_1.staticString)(n);if(staticText!==null){return staticText.endsWith("/")||staticText.endsWith("\\")}return false};if(endsWithSep(arg))return true;if(arg.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&arg.operator==="+"){return endsWithSep(arg.right)}if(arg.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const last=arg.quasis[arg.quasis.length-1].value.cooked;if(last.endsWith("/")||last.endsWith("\\"))return true;if(last===""){const tail=arg.expressions[arg.expressions.length-1];return tail!==void 0&&endsWithSep(tail)}return false}return false};const hasPathValidation=pathNode=>{if(pathNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){const parts=[];const collect=n=>{if(n.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&n.operator==="+"){collect(n.left);collect(n.right);return}if(n.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){n.expressions.forEach(e=>collect(e));return}if(n.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){n.arguments.forEach(a=>{if(a.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement)collect(a)});return}if(readsTaintSource(n))parts.push(n)};collect(pathNode);return parts.length>0&&parts.every(p=>p.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&hasPathValidation(p))}const varName=pathNode.name;const isValidationCall=testNode=>{if(testNode.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&testNode.operator==="!"&&testNode.argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){testNode=testNode.argument}if(testNode.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression){return false}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&testNode.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&testNode.callee.object.name===varName&&((0,eslint_devkit_1.propertyName)(testNode.callee)==="startsWith"||(0,eslint_devkit_1.propertyName)(testNode.callee)==="includes")){return(0,eslint_devkit_1.propertyName)(testNode.callee)==="includes"?true:isSeparatorAnchored(testNode.arguments[0])}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(testNode.callee)==="includes"){for(const arg of testNode.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name===varName){return true}}}if(testNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(testNode.callee)==="test"){for(const arg of testNode.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name===varName){return true}}}return false};const hasEarlyExit=consequent=>{if(consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){return consequent.body.some(stmt=>stmt.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||stmt.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement)}return consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement};let current=pathNode.parent;let foundFunctionBody=false;while(current&&!foundFunctionBody){if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){if(isValidationCall(current.test)){return true}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&¤t.parent&&(current.parent.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.parent.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.parent.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression)){foundFunctionBody=true;const blockBody=current.body;const nodeIndex=blockBody.findIndex(stmt=>{let check=pathNode;while(check){if(check===stmt)return true;check=check.parent}return false});for(let i=0;i<nodeIndex;i++){const stmt=blockBody[i];if(stmt.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&isValidationCall(stmt.test)&&hasEarlyExit(stmt.consequent)){return true}}}current=current.parent}return false};const fsNamespaces=new Set(["fs"]);const fsNamedMethods=new Map;const pendingCalls=[];const constBindings=new Map;function bindFsName(local,imported){if(imported==="promises")fsNamespaces.add(local);else fsNamedMethods.set(local,imported)}const checkFsCall=node=>{let methodName=fsMethodName(node.callee,fsNamespaces,fsNamedMethods);if(methodName===void 0){const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,context.sourceCode.getScope(node),{equivalents:FS_MODULE_EQUIVALENTS});if(binding?.module!=="fs")return;const[first,second]=binding.path;methodName=binding.path.length===1?first:binding.path.length===2&&first==="promises"?second:void 0;if(methodName===void 0)return}if(!dangerousMethods.has(methodName)){return}const method=methodName;const indices=PATH_ARGUMENT_INDICES.get(method)??[0];const sourceCode=context.sourceCode;let pathNode=null;let path="";for(const index of indices){const candidate=node.arguments[index];if(candidate===void 0||candidate.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)continue;if(isDangerousPath(candidate,sourceCode.getText(candidate))){pathNode=candidate;path=sourceCode.getText(candidate);break}}if(pathNode===null){const present=indices.some(index=>node.arguments[index]!==void 0);if(!present&&isDangerousPath(null,"")){context.report({node,messageId:"fsPathTraversal",data:{method,path:"",riskLevel:"MEDIUM",vulnerability:"path traversal",safePattern:"Use path.resolve() with validation",steps:"Review file system access patterns"}})}return}const operation=FS_OPERATIONS.find(op=>op.method===method)??null;const riskLevel=(0,exports.determineRiskLevel)(operation||FS_OPERATIONS[0],path);const steps=operation?(0,exports.generateRefactoringSteps)(operation):"Review file system access patterns";const safePattern=operation?.safePattern||"Use path.resolve() with validation";context.report({node,messageId:"fsPathTraversal",data:{method,path,riskLevel,vulnerability:operation?.vulnerability||"path traversal",safePattern,steps,effort:operation?.effort||"15-20 minutes"}})};return{ImportDeclaration(node){if(!(0,exports.isFsModule)(node.source.value))return;for(const spec of node.specifiers){if(spec.type===eslint_devkit_1.AST_NODE_TYPES.ImportSpecifier){const imported=spec.imported.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?spec.imported.name:spec.imported.value;bindFsName(spec.local.name,imported);continue}fsNamespaces.add(spec.local.name)}},VariableDeclarator(node){if(node.init!==null&&node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration&&node.parent.kind==="const"){constBindings.set(node.id.name,node.init)}if(node.init===null||!isFsRequire(node.init))return;if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){fsNamespaces.add(node.id.name);return}if(node.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return;for(const prop of node.id.properties){if(prop.type!==eslint_devkit_1.AST_NODE_TYPES.Property||prop.computed)continue;if(prop.value.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)continue;const key=prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?prop.key.name:(0,eslint_devkit_1.staticString)(prop.key)??void 0;if(key===void 0)continue;bindFsName(prop.value.name,key)}},CallExpression(node){pendingCalls.push(node)},"Program:exit"(){for(const call of pendingCalls)checkFsCall(call)}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noArbitraryFileAccess=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noArbitraryFileAccess=(0,eslint_devkit_1.createRule)({name:"no-arbitrary-file-access",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-arbitrary-file-access.md",description:"Prevent file access from user input",cwe:"CWE-22",cvss:7.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Arbitrary File Access",cwe:"CWE-22",description:"File path from user input - path traversal vulnerability",severity:"HIGH",fix:"Validate and sanitize file paths, use allowlists",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"})},schema:[]},defaultOptions:[],create(context){const sourceCode=context.sourceCode;function report(node){context.report({node,messageId:"violationDetected"})}const fsReadMethods=["readFile","readFileSync","readdir","readdirSync","stat","statSync"];const fsWriteMethods=["writeFile","writeFileSync","appendFile","appendFileSync"];const userInputSources=new Set(["req","request","params","query","body"]);const bindings=new Map;function readsUserInput(node,depth=0){if(depth>6)return false;switch(node.type){case"Identifier":{if(userInputSources.has(node.name.toLowerCase()))return true;const bound=bindings.get(node.name);return bound!==void 0&&readsUserInput(bound,depth+1)}case"MemberExpression":return readsUserInput(node.object,depth+1);case"TemplateLiteral":return node.expressions.some(e=>readsUserInput(e,depth+1));case"BinaryExpression":return readsUserInput(node.left,depth+1)||readsUserInput(node.right,depth+1);case"CallExpression":return node.arguments.some(arg=>arg.type!=="SpreadElement"&&readsUserInput(arg,depth+1));default:return false}}const allCalls=[];function functionName(fn){if(fn.type!=="ArrowFunctionExpression"&&fn.id!==null)return fn.id.name;const parent=fn.parent;if(parent.type==="VariableDeclarator"&&parent.id.type==="Identifier"){return parent.id.name}if(parent.type==="Property"&&!parent.computed&&parent.key.type==="Identifier"){return parent.key.name}if(parent.type==="AssignmentExpression"&&parent.left.type==="Identifier"){return parent.left.name}return void 0}function callerPassesUserInput(name,index){return allCalls.some(call=>{const callee=call.callee;const calleeName=callee.type==="Identifier"?callee.name:callee.type==="MemberExpression"
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noArbitraryFileAccess=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noArbitraryFileAccess=(0,eslint_devkit_1.createRule)({name:"no-arbitrary-file-access",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-arbitrary-file-access.md",description:"Prevent file access from user input",cwe:"CWE-22",cvss:7.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Arbitrary File Access",cwe:"CWE-22",description:"File path from user input - path traversal vulnerability",severity:"HIGH",fix:"Validate and sanitize file paths, use allowlists",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"})},schema:[]},defaultOptions:[],create(context){const sourceCode=context.sourceCode;function report(node){context.report({node,messageId:"violationDetected"})}const fsReadMethods=["readFile","readFileSync","readdir","readdirSync","stat","statSync"];const fsWriteMethods=["writeFile","writeFileSync","appendFile","appendFileSync"];const userInputSources=new Set(["req","request","params","query","body"]);const bindings=new Map;function readsUserInput(node,depth=0){if(depth>6)return false;switch(node.type){case"Identifier":{if(userInputSources.has(node.name.toLowerCase()))return true;const bound=bindings.get(node.name);return bound!==void 0&&readsUserInput(bound,depth+1)}case"MemberExpression":return readsUserInput(node.object,depth+1);case"TemplateLiteral":return node.expressions.some(e=>readsUserInput(e,depth+1));case"BinaryExpression":return readsUserInput(node.left,depth+1)||readsUserInput(node.right,depth+1);case"CallExpression":return node.arguments.some(arg=>arg.type!=="SpreadElement"&&readsUserInput(arg,depth+1));default:return false}}const allCalls=[];function functionName(fn){if(fn.type!=="ArrowFunctionExpression"&&fn.id!==null)return fn.id.name;const parent=fn.parent;if(parent.type==="VariableDeclarator"&&parent.id.type==="Identifier"){return parent.id.name}if(parent.type==="Property"&&!parent.computed&&parent.key.type==="Identifier"){return parent.key.name}if(parent.type==="AssignmentExpression"&&parent.left.type==="Identifier"){return parent.left.name}return void 0}function callerPassesUserInput(name,index){return allCalls.some(call=>{const callee=call.callee;const calleeName=callee.type==="Identifier"?callee.name:callee.type==="MemberExpression"?(0,eslint_devkit_1.propertyName)(callee)??void 0:void 0;if(calleeName!==name)return false;const arg=call.arguments[index];return arg!==void 0&&arg.type!=="SpreadElement"&&readsUserInput(arg)})}function variableTracesToUserInput(varName,from){if(userInputSources.has(varName.toLowerCase()))return true;let scope=from;while(scope!=null){if(scope.type==="FunctionDeclaration"||scope.type==="FunctionExpression"||scope.type==="ArrowFunctionExpression"){const index=scope.params.findIndex(param=>param.type==="Identifier"&¶m.name===varName);if(index!==-1){const name=functionName(scope);return name!==void 0&&callerPassesUserInput(name,index)}}const body=scope.type==="Program"||scope.type==="BlockStatement"?scope.body:void 0;if(body!==void 0){for(const stmt of body){if(stmt.type!=="VariableDeclaration")continue;for(const decl of stmt.declarations){if(decl.id.type!=="Identifier"||decl.id.name!==varName)continue;return decl.init!=null&&readsUserInput(decl.init)}}}scope=scope.parent}return false}const sanitizedVariables=new Set;const validatedVariables=new Set;function checkVariableDeclaration(node){if(node.id.type!=="Identifier"||!node.init){return}const varName=node.id.name;const init=node.init;bindings.set(varName,init);if(init.type==="CallExpression"&&init.callee.type==="MemberExpression"&&init.callee.object.type==="Identifier"&&init.callee.object.name==="path"&&(0,eslint_devkit_1.propertyName)(init.callee)==="basename"){sanitizedVariables.add(varName)}if(init.type==="CallExpression"&&init.callee.type==="MemberExpression"&&init.callee.object.type==="Identifier"&&init.callee.object.name==="path"&&(0,eslint_devkit_1.propertyName)(init.callee)==="join"){const hasSanitizedArg=init.arguments.some(arg=>arg.type==="Identifier"&&sanitizedVariables.has(arg.name));const firstArg=init.arguments[0];const hasSafeBase=firstArg&&(firstArg.type==="Literal"||firstArg.type==="Identifier"&&/^(SAFE|BASE|ROOT|UPLOAD|PUBLIC)/i.test(firstArg.name));if(hasSanitizedArg&&hasSafeBase){sanitizedVariables.add(varName)}}}function hasStartsWithGuard(node,varName){if(validatedVariables.has(varName)){return true}let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){const statements=current.body;for(const stmt of statements){if(stmt.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){const testText=sourceCode.getText(stmt.test).toLowerCase();if(testText.includes("startswith")&&testText.includes(varName.toLowerCase())){const consequent=stmt.consequent;if(consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&&consequent.body.length>0){const firstStmt=consequent.body[0];if(firstStmt.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||firstStmt.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement){validatedVariables.add(varName);return true}}if(consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement){validatedVariables.add(varName);return true}}}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){const testText=sourceCode.getText(current.test).toLowerCase();if(testText.includes("startswith")&&testText.includes(varName.toLowerCase())){validatedVariables.add(varName);return true}}current=current.parent}return false}function isVariableSafe(varName,node){if(sanitizedVariables.has(varName)){return true}if(hasStartsWithGuard(node,varName)){return true}if(/^(safe|sanitized|validated|clean)/i.test(varName)){return true}return false}return{VariableDeclarator(node){checkVariableDeclaration(node)},CallExpression(node){allCalls.push(node)},"Program:exit"(){for(const node of allCalls)checkFsCall(node)}};function checkFsCall(node){if(node.callee.type==="MemberExpression"&&node.callee.object.type==="Identifier"&&node.callee.object.name==="fs"&&[...fsReadMethods,...fsWriteMethods].includes((0,eslint_devkit_1.propertyName)(node.callee)??"")){const pathArg=node.arguments[0];if(pathArg&&pathArg.type==="Literal"){return}if(pathArg&&pathArg.type==="Identifier"){const varName=pathArg.name;if(isVariableSafe(varName,node)){return}if(!variableTracesToUserInput(varName,node)){return}report(node);return}if(pathArg!==void 0&&readsUserInput(pathArg)){report(node)}}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noBufferOverread=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const DEFAULT_BUFFER_METHODS=["readUInt8","readInt8","readUInt16LE","readUInt16BE","readInt16LE","readInt16BE","readUInt32LE","readUInt32BE","readInt32LE","readInt32BE","readBigUInt64LE","readBigUInt64BE","readBigInt64LE","readBigInt64BE","readFloatLE","readFloatBE","readDoubleLE","readDoubleBE","readUIntLE","readUIntBE","readIntLE","readIntBE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","subarray","copy"];const DEFAULT_UNTRUSTED_SOURCES=["req","request","event","ctx","context"];const DEFAULT_BUFFER_PARAMETER_NAMES=["buf","buffer","bytes"];function isBufferReturningRead(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;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;return name==="readFileSync"&&node.arguments.length===1}const VIEW_METHODS=new Set(["slice","subarray"]);exports.noBufferOverread=(0,eslint_devkit_1.createRule)({name:"no-buffer-overread",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-buffer-overread.md",description:"Detects buffer access beyond bounds",cwe:"CWE-126"},messages:{boundsCheckDisabled:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer bounds check disabled",cwe:"CWE-125",owasp:"A06:2021",cvss:7.5,description:"The deprecated noAssert argument is true, which turns off the bounds check on this read. Past the end of the buffer it returns whatever memory follows instead of throwing.",severity:"HIGH",compliance:["SOC2","PCI-DSS","ISO27001"],fix:"Drop the noAssert argument and let the read throw on an out-of-range offset.",documentationLink:"https://cwe.mitre.org/data/definitions/125.html"}),unsafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Access",cwe:"CWE-126",description:"Buffer accessed without bounds validation",severity:"HIGH",fix:"Add bounds check before buffer access",documentationLink:"https://nodejs.org/api/buffer.html"}),missingBoundsCheck:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Bounds Check",cwe:"CWE-126",description:"Buffer operation missing bounds validation",severity:"MEDIUM",fix:"Validate indices before buffer operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),negativeBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Negative Buffer Index",cwe:"CWE-126",description:"Negative index used for buffer access",severity:"MEDIUM",fix:"Ensure buffer indices are non-negative",documentationLink:"https://nodejs.org/api/buffer.html"}),userControlledBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"User Controlled Buffer Index",cwe:"CWE-126",description:"Buffer accessed with user-controlled index",severity:"HIGH",fix:"Validate user input before using as buffer index",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferSlice:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Slice",cwe:"CWE-126",description:"Buffer slice with unvalidated indices",severity:"MEDIUM",fix:"Validate slice start/end indices",documentationLink:"https://nodejs.org/api/buffer.html#bufslicestart-end"})},schema:[{type:"object",properties:{bufferMethods:{type:"array",items:{type:"string"},default:[...DEFAULT_BUFFER_METHODS],description:"Buffer read/write methods checked for bounds"},boundsCheckFunctions:{type:"array",items:{type:"string"},default:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],description:"Function names that count as a bounds check"},bufferTypes:{type:"array",items:{type:"string"},default:["Buffer","Uint8Array","ArrayBuffer","DataView"],description:"Constructor names treated as buffer types"},trustedSanitizers:{type:"array",items:{type:"string"},default:[],description:"Additional function names to consider as buffer index validators"},trustedAnnotations:{type:"array",items:{type:"string"},default:[],description:"Additional JSDoc annotations to consider as safe markers"},untrustedSources:{type:"array",items:{type:"string"},default:[...DEFAULT_UNTRUSTED_SOURCES],description:"Identifier roots treated as carrying an inbound request (default: req, request, event, ctx, context). Replaces the list; [] disables root-based taint."},bufferParameterNames:{type:"array",items:{type:"string"},default:[...DEFAULT_BUFFER_PARAMETER_NAMES],description:"Parameter spellings treated as a Buffer when the type is not otherwise visible (default: buf, buffer, bytes). Whole-name match, never substring. Replaces the list; [] drops the convention."},strictMode:{type:"boolean",default:false,description:"Disable all false positive detection (strict mode)"},reportUnvalidatedIndices:{type:"boolean",default:false,description:"Report every index that cannot be proven validated. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{bufferMethods:[...DEFAULT_BUFFER_METHODS],boundsCheckFunctions:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes:["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers:[],trustedAnnotations:[],strictMode:false,untrustedSources:[...DEFAULT_UNTRUSTED_SOURCES],bufferParameterNames:[...DEFAULT_BUFFER_PARAMETER_NAMES]}],create(context){const options=context.options[0]||{};const{bufferMethods=[...DEFAULT_BUFFER_METHODS],boundsCheckFunctions=["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes=["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers=[],trustedAnnotations=[],strictMode=false,reportUnvalidatedIndices=false,untrustedSources=[...DEFAULT_UNTRUSTED_SOURCES],bufferParameterNames=[...DEFAULT_BUFFER_PARAMETER_NAMES]}=options;const bufferParameterSet=new Set(bufferParameterNames.map(name=>name.toLowerCase()));const sourceCode=context.sourceCode;const filename=context.filename;const safetyChecker=(0,eslint_devkit_1.createSafetyChecker)({trustedSanitizers,trustedAnnotations,trustedOrmPatterns:[],strictMode});const passedTrustedSanitizer=indexNode=>{if(trustedSanitizers.length===0)return false;if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const init=(0,provenance_1.bindingInit)(sourceCode,indexNode);if(init===void 0||init.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const calleeNode=init.callee;if(calleeNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return trustedSanitizers.includes(calleeNode.name)}return calleeNode.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&calleeNode.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&trustedSanitizers.includes(calleeNode.property.name)};const readsTaintSource=(0,provenance_1.makeReadsTaintSource)(sourceCode,new Set(untrustedSources.map(source=>source.toLowerCase())));const bufferVars=new Set;const addBufferVar=id=>{const variable=(0,provenance_1.findVariable)(sourceCode,id);if(variable)bufferVars.add(variable)};const isBufferType=node=>{const variable=(0,provenance_1.findVariable)(sourceCode,node);if(variable&&bufferVars.has(variable))return true;if(variable!==null&&variable.defs.length>0){return variable.defs[0].type==="Parameter"&&bufferParameterSet.has(node.name.toLowerCase())}return bufferParameterSet.has(node.name.toLowerCase())};const isWriteTarget=node=>{const parent=node.parent;if(!parent)return false;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression)return parent.left===node;return parent.type===eslint_devkit_1.AST_NODE_TYPES.UpdateExpression};const isLoopBounded=indexNode=>{if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const name=indexNode.name;let current=indexNode.parent;while(current){const test=current.type===eslint_devkit_1.AST_NODE_TYPES.ForStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.WhileStatement?current.test:null;if(test&&test.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&(test.operator==="<"||test.operator==="<=")&&test.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&test.left.name===name){return true}current=current.parent}return false};const isUserControlledIndex=indexNode=>readsTaintSource(indexNode);const isIndexValidated=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return typeof indexNode.value==="number"&&indexNode.value>=0}if(isBoundsCheckCall(indexNode))return true;if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const variable=(0,provenance_1.findVariable)(sourceCode,indexNode);if(variable!==null&&variable.defs[0]?.type==="Parameter")return true;const init=(0,provenance_1.bindingInit)(sourceCode,indexNode);return init!==void 0&&isBoundsCheckCall(init)};const isBoundsCheckCall=node=>{if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return boundsCheckFunctions.includes(callee.name)}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return false}return callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="Math"&&(callee.property.name==="min"||callee.property.name==="max")||boundsCheckFunctions.includes(callee.property.name)};const hasBoundsCheck=(buffer,indexNode)=>{const guards=[];collectComparisons(enclosingBody(indexNode),guards);return guards.some(test=>mentions(test.left,indexNode)&&readsLengthOf(test.right,buffer)||mentions(test.right,indexNode)&&readsLengthOf(test.left,buffer))};const sameBinding=(a,b)=>{const left=(0,provenance_1.findVariable)(sourceCode,a);const right=(0,provenance_1.findVariable)(sourceCode,b);if(left===null&&right===null)return a.name===b.name;return left===right};const enclosingBody=node=>{let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.Program){return current}current=current.parent}return node};const collectComparisons=(node,out)=>{if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&["<","<=",">",">="].includes(node.operator)){out.push(node)}for(const key of Object.keys(node)){if(key==="parent")continue;const value=node[key];for(const child of Array.isArray(value)?value:[value]){if(child!==null&&typeof child==="object"&&typeof child.type==="string"){collectComparisons(child,out)}}}};const mentions=(node,indexNode)=>{if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const walk=current=>{if(current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return sameBinding(current,indexNode)}if(current.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return walk(current.left)||walk(current.right)}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){return current.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&walk(argument))}return false};return walk(node)};const readsLengthOf=(node,buffer)=>node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.property.name==="length"&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&sameBinding(node.object,buffer);const couldBeNegative=indexNode=>{const value=constantNumber(indexNode,0);return value!==null&&value<0};const constantNumber=(node,depth)=>{if(depth>6)return null;if(node.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression){const inner=constantNumber(node.argument,depth+1);if(inner===null)return null;return node.operator==="-"?-inner:node.operator==="+"?inner:null}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){if(node.operator!=="-"&&node.operator!=="+")return null;const left=constantNumber(node.left,depth+1);const right=constantNumber(node.right,depth+1);if(left===null||right===null)return null;return node.operator==="-"?left-right:left+right}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,const_value_1.constInitializerOf)(sourceCode,node);return init===null?null:constantNumber(init,depth+1)}const resolved=(0,const_value_1.resolveConstant)(sourceCode,node);return resolved!==null&&typeof resolved.value==="number"?resolved.value:null};return{'CallExpression[callee.type="MemberExpression"][callee.computed=false][callee.property.type="Identifier"]'(node){const callee=node.callee;const method=callee.property.name;if(!/^(?:read|write)[A-Z]/.test(method))return;const flag=node.arguments.at(-1);if(node.arguments.length>=2&&flag?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&flag.value===true){context.report({node:flag,messageId:"boundsCheckDisabled"})}},VariableDeclarator(node){if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init){if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferTypes.includes(node.init.callee.name)){addBufferVar(node.id)}if(isBufferReturningRead(node.init)){addBufferVar(node.id)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init.callee.object.name==="Buffer"&&node.init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["from","alloc","allocUnsafe"].includes(node.init.callee.property.name)){addBufferVar(node.id)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.init.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object)){addBufferVar(node.id)}}}},MemberExpression(node){if(node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const buffer=node.object;const indexNode=node.property;if(isWriteTarget(node))return;if(isLoopBounded(indexNode))return;if(isBufferType(node.object)){if(couldBeNegative(indexNode)){context.report({node,messageId:"negativeBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}if(isUserControlledIndex(indexNode)&&!isIndexValidated(indexNode)){if(!hasBoundsCheck(buffer,indexNode)){if(safetyChecker.isSafe(node,context)){return}if(passedTrustedSanitizer(indexNode)){return}context.report({node,messageId:"userControlledBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}}if(reportUnvalidatedIndices&&!hasBoundsCheck(buffer,indexNode)&&!isIndexValidated(indexNode)){if(safetyChecker.isSafe(node,context)){return}if(passedTrustedSanitizer(indexNode)){return}context.report({node,messageId:"unsafeBufferAccess",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(node.property.type==="Identifier"&&bufferMethods.includes(node.property.name)&&node.object.type==="Identifier"&&isBufferType(node.object)){}},CallExpression(node){const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&VIEW_METHODS.has(callee.property.name)&&callee.object.type==="Identifier"&&isBufferType(callee.object)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)||passedTrustedSanitizer(arg)){continue}if(hasBoundsCheck(callee.object,arg)){continue}context.report({node:arg,messageId:"unsafeBufferSlice",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)&&!VIEW_METHODS.has(callee.property.name)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)||passedTrustedSanitizer(arg)){continue}if(hasBoundsCheck(callee.object,arg)){continue}context.report({node:arg,messageId:"missingBoundsCheck",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}},BinaryExpression(node){const leftText=sourceCode.getText(node.left);const rightText=sourceCode.getText(node.right);if(leftText.includes(".length")||rightText.includes(".length")){}}}}});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noBufferOverread=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const DEFAULT_BUFFER_METHODS=["readUInt8","readInt8","readUInt16LE","readUInt16BE","readInt16LE","readInt16BE","readUInt32LE","readUInt32BE","readInt32LE","readInt32BE","readBigUInt64LE","readBigUInt64BE","readBigInt64LE","readBigInt64BE","readFloatLE","readFloatBE","readDoubleLE","readDoubleBE","readUIntLE","readUIntBE","readIntLE","readIntBE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","subarray","copy"];const DEFAULT_UNTRUSTED_SOURCES=["req","request","event","ctx","context"];const DEFAULT_BUFFER_PARAMETER_NAMES=["buf","buffer","bytes"];function isBufferReturningRead(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;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?(0,eslint_devkit_1.propertyName)(callee):null;return name==="readFileSync"&&node.arguments.length===1}const VIEW_METHODS=new Set(["slice","subarray"]);exports.noBufferOverread=(0,eslint_devkit_1.createRule)({name:"no-buffer-overread",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-buffer-overread.md",description:"Detects buffer access beyond bounds",cwe:"CWE-126"},messages:{boundsCheckDisabled:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer bounds check disabled",cwe:"CWE-125",owasp:"A06:2021",cvss:7.5,description:"The deprecated noAssert argument is true, which turns off the bounds check on this read. Past the end of the buffer it returns whatever memory follows instead of throwing.",severity:"HIGH",compliance:["SOC2","PCI-DSS","ISO27001"],fix:"Drop the noAssert argument and let the read throw on an out-of-range offset.",documentationLink:"https://cwe.mitre.org/data/definitions/125.html"}),unsafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Access",cwe:"CWE-126",description:"Buffer accessed without bounds validation",severity:"HIGH",fix:"Add bounds check before buffer access",documentationLink:"https://nodejs.org/api/buffer.html"}),missingBoundsCheck:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Bounds Check",cwe:"CWE-126",description:"Buffer operation missing bounds validation",severity:"MEDIUM",fix:"Validate indices before buffer operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),negativeBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Negative Buffer Index",cwe:"CWE-126",description:"Negative index used for buffer access",severity:"MEDIUM",fix:"Ensure buffer indices are non-negative",documentationLink:"https://nodejs.org/api/buffer.html"}),userControlledBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"User Controlled Buffer Index",cwe:"CWE-126",description:"Buffer accessed with user-controlled index",severity:"HIGH",fix:"Validate user input before using as buffer index",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferSlice:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Slice",cwe:"CWE-126",description:"Buffer slice with unvalidated indices",severity:"MEDIUM",fix:"Validate slice start/end indices",documentationLink:"https://nodejs.org/api/buffer.html#bufslicestart-end"})},schema:[{type:"object",properties:{bufferMethods:{type:"array",items:{type:"string"},default:[...DEFAULT_BUFFER_METHODS],description:"Buffer read/write methods checked for bounds"},boundsCheckFunctions:{type:"array",items:{type:"string"},default:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],description:"Function names that count as a bounds check"},bufferTypes:{type:"array",items:{type:"string"},default:["Buffer","Uint8Array","ArrayBuffer","DataView"],description:"Constructor names treated as buffer types"},trustedSanitizers:{type:"array",items:{type:"string"},default:[],description:"Additional function names to consider as buffer index validators"},trustedAnnotations:{type:"array",items:{type:"string"},default:[],description:"Additional JSDoc annotations to consider as safe markers"},untrustedSources:{type:"array",items:{type:"string"},default:[...DEFAULT_UNTRUSTED_SOURCES],description:"Identifier roots treated as carrying an inbound request (default: req, request, event, ctx, context). Replaces the list; [] disables root-based taint."},bufferParameterNames:{type:"array",items:{type:"string"},default:[...DEFAULT_BUFFER_PARAMETER_NAMES],description:"Parameter spellings treated as a Buffer when the type is not otherwise visible (default: buf, buffer, bytes). Whole-name match, never substring. Replaces the list; [] drops the convention."},strictMode:{type:"boolean",default:false,description:"Disable all false positive detection (strict mode)"},reportUnvalidatedIndices:{type:"boolean",default:false,description:"Report every index that cannot be proven validated. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{bufferMethods:[...DEFAULT_BUFFER_METHODS],boundsCheckFunctions:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes:["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers:[],trustedAnnotations:[],strictMode:false,untrustedSources:[...DEFAULT_UNTRUSTED_SOURCES],bufferParameterNames:[...DEFAULT_BUFFER_PARAMETER_NAMES]}],create(context){const options=context.options[0]||{};const{bufferMethods=[...DEFAULT_BUFFER_METHODS],boundsCheckFunctions=["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes=["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers=[],trustedAnnotations=[],strictMode=false,reportUnvalidatedIndices=false,untrustedSources=[...DEFAULT_UNTRUSTED_SOURCES],bufferParameterNames=[...DEFAULT_BUFFER_PARAMETER_NAMES]}=options;const bufferParameterSet=new Set(bufferParameterNames.map(name=>name.toLowerCase()));const sourceCode=context.sourceCode;const filename=context.filename;const safetyChecker=(0,eslint_devkit_1.createSafetyChecker)({trustedSanitizers,trustedAnnotations,trustedOrmPatterns:[],strictMode});const passedTrustedSanitizer=indexNode=>{if(trustedSanitizers.length===0)return false;if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const init=(0,provenance_1.bindingInit)(sourceCode,indexNode);if(init===void 0||init.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const calleeNode=init.callee;if(calleeNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return trustedSanitizers.includes(calleeNode.name)}return calleeNode.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(calleeNode),trustedSanitizers)};const readsTaintSource=(0,provenance_1.makeReadsTaintSource)(sourceCode,new Set(untrustedSources.map(source=>source.toLowerCase())));const bufferVars=new Set;const addBufferVar=id=>{const variable=(0,provenance_1.findVariable)(sourceCode,id);if(variable)bufferVars.add(variable)};const isBufferType=node=>{const variable=(0,provenance_1.findVariable)(sourceCode,node);if(variable&&bufferVars.has(variable))return true;if(variable!==null&&variable.defs.length>0){return variable.defs[0].type==="Parameter"&&bufferParameterSet.has(node.name.toLowerCase())}return bufferParameterSet.has(node.name.toLowerCase())};const isWriteTarget=node=>{const parent=node.parent;if(!parent)return false;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression)return parent.left===node;return parent.type===eslint_devkit_1.AST_NODE_TYPES.UpdateExpression};const isLoopBounded=indexNode=>{if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const name=indexNode.name;let current=indexNode.parent;while(current){const test=current.type===eslint_devkit_1.AST_NODE_TYPES.ForStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.WhileStatement?current.test:null;if(test&&test.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&(test.operator==="<"||test.operator==="<=")&&test.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&test.left.name===name){return true}current=current.parent}return false};const isUserControlledIndex=indexNode=>readsTaintSource(indexNode);const isIndexValidated=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return typeof indexNode.value==="number"&&indexNode.value>=0}if(isBoundsCheckCall(indexNode))return true;if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const variable=(0,provenance_1.findVariable)(sourceCode,indexNode);if(variable!==null&&variable.defs[0]?.type==="Parameter")return true;const init=(0,provenance_1.bindingInit)(sourceCode,indexNode);return init!==void 0&&isBoundsCheckCall(init)};const isBoundsCheckCall=node=>{if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return boundsCheckFunctions.includes(callee.name)}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||(0,eslint_devkit_1.propertyName)(callee)===null){return false}return callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="Math"&&((0,eslint_devkit_1.propertyName)(callee)==="min"||(0,eslint_devkit_1.propertyName)(callee)==="max")||(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),boundsCheckFunctions)};const hasBoundsCheck=(buffer,indexNode)=>{const guards=[];collectComparisons(enclosingBody(indexNode),guards);return guards.some(test=>mentions(test.left,indexNode)&&readsLengthOf(test.right,buffer)||mentions(test.right,indexNode)&&readsLengthOf(test.left,buffer))};const sameBinding=(a,b)=>{const left=(0,provenance_1.findVariable)(sourceCode,a);const right=(0,provenance_1.findVariable)(sourceCode,b);if(left===null&&right===null)return a.name===b.name;return left===right};const enclosingBody=node=>{let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.Program){return current}current=current.parent}return node};const collectComparisons=(node,out)=>{if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&["<","<=",">",">="].includes(node.operator)){out.push(node)}for(const key of Object.keys(node)){if(key==="parent")continue;const value=node[key];for(const child of Array.isArray(value)?value:[value]){if(child!==null&&typeof child==="object"&&typeof child.type==="string"){collectComparisons(child,out)}}}};const mentions=(node,indexNode)=>{if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const walk=current=>{if(current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return sameBinding(current,indexNode)}if(current.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return walk(current.left)||walk(current.right)}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){return current.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&walk(argument))}return false};return walk(node)};const readsLengthOf=(node,buffer)=>node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(node)==="length"&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&sameBinding(node.object,buffer);const couldBeNegative=indexNode=>{const value=constantNumber(indexNode,0);return value!==null&&value<0};const constantNumber=(node,depth)=>{if(depth>6)return null;if(node.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression){const inner=constantNumber(node.argument,depth+1);if(inner===null)return null;return node.operator==="-"?-inner:node.operator==="+"?inner:null}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){if(node.operator!=="-"&&node.operator!=="+")return null;const left=constantNumber(node.left,depth+1);const right=constantNumber(node.right,depth+1);if(left===null||right===null)return null;return node.operator==="-"?left-right:left+right}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,const_value_1.constInitializerOf)(sourceCode,node);return init===null?null:constantNumber(init,depth+1)}const resolved=(0,const_value_1.resolveConstant)(sourceCode,node);return resolved!==null&&typeof resolved.value==="number"?resolved.value:null};return{'CallExpression[callee.type="MemberExpression"]'(node){const callee=node.callee;const method=(0,eslint_devkit_1.propertyName)(callee);if(method===null||!/^(?:read|write)[A-Z]/.test(method))return;const flag=node.arguments.at(-1);if(node.arguments.length>=2&&flag?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&flag.value===true){context.report({node:flag,messageId:"boundsCheckDisabled"})}},VariableDeclarator(node){if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init){if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferTypes.includes(node.init.callee.name)){addBufferVar(node.id)}if(isBufferReturningRead(node.init)){addBufferVar(node.id)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init.callee.object.name==="Buffer"&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node.init.callee),["from","alloc","allocUnsafe"])){addBufferVar(node.id)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.init.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),bufferMethods)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object)){addBufferVar(node.id)}}}},MemberExpression(node){if(node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const buffer=node.object;const indexNode=node.property;if(isWriteTarget(node))return;if(isLoopBounded(indexNode))return;if(isBufferType(node.object)){if(couldBeNegative(indexNode)){context.report({node,messageId:"negativeBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}if(isUserControlledIndex(indexNode)&&!isIndexValidated(indexNode)){if(!hasBoundsCheck(buffer,indexNode)){if(safetyChecker.isSafe(node,context)){return}if(passedTrustedSanitizer(indexNode)){return}context.report({node,messageId:"userControlledBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}}if(reportUnvalidatedIndices&&!hasBoundsCheck(buffer,indexNode)&&!isIndexValidated(indexNode)){if(safetyChecker.isSafe(node,context)){return}if(passedTrustedSanitizer(indexNode)){return}context.report({node,messageId:"unsafeBufferAccess",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if((0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node),bufferMethods)&&node.object.type==="Identifier"&&isBufferType(node.object)){}},CallExpression(node){const callee=node.callee;if(callee.type==="MemberExpression"&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),VIEW_METHODS)&&callee.object.type==="Identifier"&&isBufferType(callee.object)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)||passedTrustedSanitizer(arg)){continue}if(hasBoundsCheck(callee.object,arg)){continue}context.report({node:arg,messageId:"unsafeBufferSlice",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),bufferMethods)&&!(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),VIEW_METHODS)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)||passedTrustedSanitizer(arg)){continue}if(hasBoundsCheck(callee.object,arg)){continue}context.report({node:arg,messageId:"missingBoundsCheck",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}},BinaryExpression(node){const leftText=sourceCode.getText(node.left);const rightText=sourceCode.getText(node.right);if(leftText.includes(".length")||rightText.includes(".length")){}}}}});
|