eslint-plugin-node-security 4.11.0 → 4.12.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/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-node-security",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.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",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
9
10
|
".": {
|
|
10
11
|
"types": "./src/index.d.ts",
|
|
11
12
|
"default": "./src/index.js"
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
"node": ">=18.0.0"
|
|
79
80
|
},
|
|
80
81
|
"dependencies": {
|
|
81
|
-
"@interlace/eslint-devkit": "^1.
|
|
82
|
+
"@interlace/eslint-devkit": "^1.15.0"
|
|
82
83
|
},
|
|
83
84
|
"peerDependencies": {
|
|
84
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;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}};exports.plugin={meta:{name:"eslint-plugin-node-security",version:"4.
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.configs=exports.plugin=exports.rules=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.rules={get"detect-child-process"(){return require("./rules/detect-child-process").detectChildProcess},get"detect-eval-with-expression"(){return require("./rules/detect-eval-with-expression").detectEvalWithExpression},get"detect-non-literal-fs-filename"(){return require("./rules/detect-non-literal-fs-filename").detectNonLiteralFsFilename},get"no-unsafe-dynamic-require"(){return require("./rules/no-unsafe-dynamic-require").noUnsafeDynamicRequire},get"no-buffer-overread"(){return require("./rules/no-buffer-overread").noBufferOverread},get"no-deprecated-buffer"(){return require("./rules/no-deprecated-buffer").noDeprecatedBuffer},get"no-unsafe-buffer-alloc"(){return require("./rules/no-unsafe-buffer-alloc").noUnsafeBufferAlloc},get"no-toctou-vulnerability"(){return require("./rules/no-toctou-vulnerability").noToctouVulnerability},get"no-zip-slip"(){return require("./rules/no-zip-slip").noZipSlip},get"no-arbitrary-file-access"(){return require("./rules/no-arbitrary-file-access").noArbitraryFileAccess},get"no-data-in-temp-storage"(){return require("./rules/no-data-in-temp-storage").noDataInTempStorage},get"no-ssrf"(){return require("./rules/no-ssrf").noSsrf},get"no-shell-injection"(){return require("./rules/no-shell-injection").noShellInjection},get"no-dynamic-command-string"(){return require("./rules/no-dynamic-command-string").noDynamicCommandString},get"no-env-injection"(){return require("./rules/no-env-injection").noEnvInjection},get"no-dynamic-algorithm-selection"(){return require("./rules/no-dynamic-algorithm-selection").noDynamicAlgorithmSelection},get"detect-suspicious-dependencies"(){return require("./rules/detect-suspicious-dependencies").detectSuspiciousDependencies},get"lock-file"(){return require("./rules/lock-file").lockFile},get"no-dynamic-dependency-loading"(){return require("./rules/no-dynamic-dependency-loading").noDynamicDependencyLoading},get"require-dependency-integrity"(){return require("./rules/require-dependency-integrity").requireDependencyIntegrity},get"require-secure-credential-storage"(){return require("./rules/require-secure-credential-storage").requireSecureCredentialStorage},get"require-secure-deletion"(){return require("./rules/require-secure-deletion").requireSecureDeletion},get"require-storage-encryption"(){return require("./rules/require-storage-encryption").requireStorageEncryption},get"no-dynamic-require"(){return require("./rules/no-dynamic-require").noDynamicRequire},get"no-cryptojs"(){return require("./rules/no-cryptojs").noCryptojs},get"no-cryptojs-weak-random"(){return require("./rules/no-cryptojs-weak-random").noCryptojsWeakRandom},get"no-deprecated-cipher-method"(){return require("./rules/no-deprecated-cipher-method").noDeprecatedCipherMethod},get"no-ecb-mode"(){return require("./rules/no-ecb-mode").noEcbMode},get"no-insecure-key-derivation"(){return require("./rules/no-insecure-key-derivation").noInsecureKeyDerivation},get"no-insecure-rsa-padding"(){return require("./rules/no-insecure-rsa-padding").noInsecureRsaPadding},get"no-math-random-crypto"(){return require("./rules/no-math-random-crypto").noMathRandomCrypto},get"no-self-signed-certs"(){return require("./rules/no-self-signed-certs").noSelfSignedCerts},get"no-sha1-hash"(){return require("./rules/no-sha1-hash").noSha1Hash},get"no-static-iv"(){return require("./rules/no-static-iv").noStaticIv},get"no-timing-unsafe-compare"(){return require("./rules/no-timing-unsafe-compare").noTimingUnsafeCompare},get"no-weak-cipher-algorithm"(){return require("./rules/no-weak-cipher-algorithm").noWeakCipherAlgorithm},get"no-weak-hash-algorithm"(){return require("./rules/no-weak-hash-algorithm").noWeakHashAlgorithm},get"prefer-native-crypto"(){return require("./rules/prefer-native-crypto").preferNativeCrypto},get"require-aead-tag-verification"(){return require("./rules/require-aead-tag-verification").requireAeadTagVerification},get"no-unbounded-decompression"(){return require("./rules/no-unbounded-decompression").noUnboundedDecompression},get"no-insecure-http-parser"(){return require("./rules/no-insecure-http-parser").noInsecureHttpParser},get"require-stream-error-handler"(){return require("./rules/require-stream-error-handler").requireStreamErrorHandler}};(0,eslint_devkit_1.withCanonicalDocsUrls)("plugin-node-security",exports.rules);exports.plugin={meta:{name:"eslint-plugin-node-security",version:"4.12.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 SHELL_BINARIES=new Set(["sh","bash","zsh","dash","ksh","csh","tcsh","fish","ash","busybox","cmd","cmd.exe","powershell","powershell.exe","pwsh","pwsh.exe","env"]);const EVAL_FLAGS=new Set(["-c","-e","--eval","-e:","/c","/k","-command","-encodedcommand"]);const COMMAND_PATTERNS=[{method:"exec",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFile","spawn"],example:{bad:"exec(`git clone ${repoUrl}`)",good:["execFile('git', ['clone', repoUrl], {shell: false})","spawn('git', ['clone', repoUrl], {shell: false})"]},effort:"15-25 minutes"},{method:"execSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFileSync","spawnSync"],example:{bad:"execSync(`npm install ${packageName}`)",good:["execFileSync('npm', ['install', packageName], {shell: false})","spawnSync('npm', ['install', packageName], {shell: false})"]},effort:"15-25 minutes"},{method:"spawn",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawn with validation"],example:{bad:"spawn('bash', ['-c', userCommand])",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"20-30 minutes"},{method:"execFile",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"execFile(userCommand, userArgs, callback)",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"execFileSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"execFileSync(userCommand, userArgs)",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"spawnSync",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawnSync with validation"],example:{bad:"spawnSync('bash', ['-c', userCommand])",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"15-20 minutes"},{method:"fork",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"fork(userScript)",good:["spawn('node', [validatedScript], {shell: false})","// Validate script path first"]},effort:"15-20 minutes"},{method:"forkSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"forkSync(userScript)",good:["spawnSync('node', [validatedScript], {shell: false, stdio: 'inherit'})","// Validate script path first"]},effort:"15-20 minutes"}];const generateRefactoringSteps=pattern=>{switch(pattern.method){case"exec":case"execSync":return[" 1. Replace exec() with execFile() or spawn()"," 2. Split command and arguments into separate array elements"," 3. Use {shell: false} option to prevent shell interpretation"," 4. Validate and sanitize all user inputs"," 5. Consider using execa library for better security"].join("\n");case"spawn":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Consider using cross-spawn for cross-platform safety"].join("\n");case"execFile":return[" 1. Replace execFile() with spawn() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"execFileSync":return[" 1. Replace execFileSync() with spawnSync() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"spawnSync":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Handle synchronous execution properly"].join("\n");case"fork":return[" 1. Replace fork() with spawn() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawn('node', [scriptPath], options) instead"," 4. Add proper error handling"," 5. Consider using child_process.execFile() for simple scripts"].join("\n");case"forkSync":return[" 1. Replace forkSync() with spawnSync() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawnSync('node', [scriptPath], options) instead"," 4. Add proper error handling and synchronous waiting"," 5. Consider using child_process.execFileSync() for simple scripts"].join("\n");default:return[" 1. Identify the specific command execution need"," 2. Choose appropriate child_process method"," 3. Use argument arrays instead of string interpolation"," 4. Add comprehensive input validation"," 5. Test with malicious inputs"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectChildProcess=(0,eslint_devkit_2.createRule)({name:"detect-child-process",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-child-process.md",description:"Detects child_process usage that may allow command injection",cwe:"CWE-78",cvss:9.8,confidence:"medium"},hasSuggestions:true,messages:{childProcessCommandInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Command injection",cwe:"CWE-78",description:"Command injection detected",severity:"CRITICAL",fix:"Use execFile/spawn with {shell: false} and array args",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),argumentInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Argument injection",cwe:"CWE-88",description:"An attacker-steered value sits in the argv vector with no `--` before it. There is no shell here, but the callee still parses a leading `-` as an option \u2014 `--upload-pack=` (git), `--to-command=` (tar), `-o ProxyCommand=` (ssh) all execute arbitrary programs.",severity:"HIGH",fix:"Insert a literal '--' before the first attacker-controlled element, or reject values beginning with '-'.",documentationLink:"https://cwe.mitre.org/data/definitions/88.html"}),useEndOfOptions:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use end-of-options",description:"Add a literal '--' before user-controlled arguments",severity:"LOW",fix:"execFile('git', ['ls-remote', '--', remote])",documentationLink:"https://cwe.mitre.org/data/definitions/88.html"}),useExecFile:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use execFile",description:"Use execFile() with argument array",severity:"LOW",fix:"execFile(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback"}),useSpawn:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use spawn",description:"Use spawn() with separate arguments",severity:"LOW",fix:"spawn(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processspawncommand-args-options"}),useSaferLibrary:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safer Library",description:"Consider safer command execution libraries",severity:"LOW",fix:"Use execa, zx, or cross-spawn instead",documentationLink:"https://github.com/sindresorhus/execa"}),validateInput:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Input",description:"Add input validation and sanitization",severity:"LOW",fix:"Validate user input before passing to command",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),useShellFalse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Disable Shell",description:"Use shell: false option",severity:"LOW",fix:"{ shell: false } to prevent shell interpretation",documentationLink:"https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Validate Strategy",description:"Comprehensive input validation",severity:"LOW",fix:"Add allowlist validation before command execution",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategySanitize:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Sanitize Strategy",description:"Sanitize and escape command arguments",severity:"LOW",fix:"Escape special characters in command arguments",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategyRestrict:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Restrict Strategy",description:"Restrict to predefined safe commands",severity:"LOW",fix:"Define allowlist of permitted commands",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow exec() with literal strings"},allowLiteralSpawn:{type:"boolean",default:false,description:"Allow spawn() with literal arguments"},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional child_process methods to check"},strategy:{type:"string",enum:["validate","sanitize","restrict","auto"],default:"auto",description:"Strategy for fixing command injection (auto = smart detection)"},taintSources:{type:"array",items:{type:"string"},default:DEFAULT_TAINT_SOURCES,description:"Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process)"},reportUnresolvedCommands:{type:"boolean",default:false,description:'Report a command whose provenance cannot be resolved. Restores the pre-inversion "any dynamic argument is dangerous" behaviour.'}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,allowLiteralSpawn:false,additionalMethods:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,allowLiteralSpawn=false,additionalMethods=[]}=options;const readsTaintSource=(0,provenance_1.makeReadsTaintSource)(context.sourceCode,new Set((options.taintSources??DEFAULT_TAINT_SOURCES).map(source=>source.toLowerCase())));const reportUnresolvedCommands=options.reportUnresolvedCommands??false;const dangerousMethodsSet=new Set(["exec","execSync","execFile","execFileSync","spawn","spawnSync","fork","forkSync",...additionalMethods]);const moduleAliases=new Set(["child_process"]);const importedMethods=new Set;const containsDynamicStrings=node=>{if(node.type==="TemplateLiteral"){return node.expressions.length>0}if(node.type==="BinaryExpression"&&node.operator==="+"){return true}if(node.type==="Identifier"){return true}return false};const hasOnlyLiteralArgs=args=>{if(args.length===0)return false;const command=args[0];if(command.type!=="Literal"||typeof command.value!=="string"){return false}if(args.length>=2){const argsArray=args[1];if(argsArray.type==="ArrayExpression"){const allLiteralElements=argsArray.elements.every(el=>el?.type==="Literal"&&typeof el.value==="string");if(!allLiteralElements){return false}}else if(argsArray.type!=="Literal"){return false}}return true};const isLiteralStringNode=argument=>argument!==void 0&&argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof argument.value==="string";const usesShell=(node,method)=>{if(method==="exec"||method==="execSync")return true;const command=node.arguments[0];if(command!==void 0&&command.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof command.value==="string"&&SHELL_BINARIES.has(command.value.replace(/^.*[/\\]/,"").toLowerCase())){return true}const argv=node.arguments[1];if(argv?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){for(const element of argv.elements){if(element?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof element.value==="string"&&EVAL_FLAGS.has(element.value.toLowerCase())){return true}}}for(const candidate of[node.arguments[1],node.arguments[2]]){if(!candidate||candidate.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)continue;for(const property of candidate.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.key.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||property.key.name!=="shell"){continue}return!(property.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value.value===false)}}return false};const cannotStartWithDash=node=>{if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const first=node.quasis[0].value.raw;return first.length>0&&!first.startsWith("-")}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"){const left=node.left;return left.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof left.value==="string"&&left.value.length>0&&!left.value.startsWith("-")}return false};const argumentInjectionSite=node=>{const argv=node.arguments[1];if(argv?.type!==eslint_devkit_1.AST_NODE_TYPES.ArrayExpression)return null;for(const element of argv.elements){if(element===null)continue;if(element.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&element.value==="--"){return null}if(!readsTaintSource(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 isDynamic=node.arguments.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 getChildProcessCall=node=>{if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(!dangerousMethodsSet.has(methodName)){return null}if(node.callee.object.type==="Identifier"&&moduleAliases.has(node.callee.object.name)){return{method:methodName,calleeNode:node.callee}}}if(node.callee.type==="Identifier"&&dangerousMethodsSet.has(node.callee.name)){if(importedMethods.has(node.callee.name)){return{method:node.callee.name,calleeNode:node.callee}}}return null};const checkChildProcessCall=node=>{const detected=getChildProcessCall(node);if(!detected){return}const{method}=detected;const{args,pattern,isDynamic}=extractCommandInfo(node,method);if((method==="exec"||method==="execSync")&&!isDynamic&&hasOnlyLiteralArgs(node.arguments)){return}if(allowLiteralStrings&&method==="exec"&&!isDynamic){return}const saferMethods=new Set(["spawn","spawnSync","execFile","execFileSync"]);if(allowLiteralSpawn&&saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){return}if(saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){const isExecFile=method==="execFile"||method==="execFileSync";if(isExecFile||hasShellFalseOption(node)){return}}const allSafeMethods=["execFile","execFileSync","spawn","spawnSync"];if(allSafeMethods.includes(method)&&hasPrecedingAllowlistValidation(node)){return}if(isLiteralStringNode(node.arguments[0])&&!usesShell(node,method)){const injected=argumentInjectionSite(node);if(injected===null)return;context.report({node:injected,messageId:"argumentInjection",suggest:[{messageId:"useEndOfOptions",fix:()=>null},{messageId:"validateInput",fix:()=>null}]});return}if(!reportUnresolvedCommands&&!node.arguments.some(argument=>readsTaintSource(argument))){return}const riskLevel=determineRiskLevel(pattern,isDynamic);const steps=pattern?(0,exports.generateRefactoringSteps)(pattern):"Review and secure command execution";const alternatives=pattern?.safeAlternatives.join(", ")||"execFile, spawn with validation";context.report({node,messageId:"childProcessCommandInjection",data:{method,args,riskLevel,vulnerability:pattern?.vulnerability||"command injection",alternatives,steps,effort:pattern?.effort||"15-30 minutes"},suggest:[{messageId:"useExecFile",fix:()=>null},{messageId:"useSpawn",fix:()=>null},{messageId:"useSaferLibrary",fix:()=>null},{messageId:"validateInput",fix:()=>null},{messageId:"useShellFalse",fix:()=>null}]})};const trackChildProcessImport=node=>{if(node.source.value!=="child_process"){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"&&node.init.arguments[0].value==="child_process"){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"&&node.init.arguments[0].value==="child_process"){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)}}}};return{CallExpression:checkChildProcessCall,ImportDeclaration:trackChildProcessImport,VariableDeclarator:trackChildProcessRequire}}});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectChildProcess=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TAINT_SOURCES=["req","request","ctx","event","process"];const LOCAL_TAINT_ROOT="process";const SHELL_BINARIES=new Set(["sh","bash","zsh","dash","ksh","csh","tcsh","fish","ash","busybox","cmd","cmd.exe","powershell","powershell.exe","pwsh","pwsh.exe","env"]);const EVAL_FLAGS=new Set(["-c","-e","--eval","-e:","/c","/k","-command","-encodedcommand"]);const COMMAND_PATTERNS=[{method:"exec",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFile","spawn"],example:{bad:"exec(`git clone ${repoUrl}`)",good:["execFile('git', ['clone', repoUrl], {shell: false})","spawn('git', ['clone', repoUrl], {shell: false})"]},effort:"15-25 minutes"},{method:"execSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFileSync","spawnSync"],example:{bad:"execSync(`npm install ${packageName}`)",good:["execFileSync('npm', ['install', packageName], {shell: false})","spawnSync('npm', ['install', packageName], {shell: false})"]},effort:"15-25 minutes"},{method:"spawn",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawn with validation"],example:{bad:"spawn('bash', ['-c', userCommand])",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"20-30 minutes"},{method:"execFile",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"execFile(userCommand, userArgs, callback)",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"execFileSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"execFileSync(userCommand, userArgs)",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"spawnSync",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawnSync with validation"],example:{bad:"spawnSync('bash', ['-c', userCommand])",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"15-20 minutes"},{method:"fork",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"fork(userScript)",good:["spawn('node', [validatedScript], {shell: false})","// Validate script path first"]},effort:"15-20 minutes"},{method:"forkSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"forkSync(userScript)",good:["spawnSync('node', [validatedScript], {shell: false, stdio: 'inherit'})","// Validate script path first"]},effort:"15-20 minutes"}];const generateRefactoringSteps=pattern=>{switch(pattern.method){case"exec":case"execSync":return[" 1. Replace exec() with execFile() or spawn()"," 2. Split command and arguments into separate array elements"," 3. Use {shell: false} option to prevent shell interpretation"," 4. Validate and sanitize all user inputs"," 5. Consider using execa library for better security"].join("\n");case"spawn":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Consider using cross-spawn for cross-platform safety"].join("\n");case"execFile":return[" 1. Replace execFile() with spawn() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"execFileSync":return[" 1. Replace execFileSync() with spawnSync() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"spawnSync":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Handle synchronous execution properly"].join("\n");case"fork":return[" 1. Replace fork() with spawn() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawn('node', [scriptPath], options) instead"," 4. Add proper error handling"," 5. Consider using child_process.execFile() for simple scripts"].join("\n");case"forkSync":return[" 1. Replace forkSync() with spawnSync() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawnSync('node', [scriptPath], options) instead"," 4. Add proper error handling and synchronous waiting"," 5. Consider using child_process.execFileSync() for simple scripts"].join("\n");default:return[" 1. Identify the specific command execution need"," 2. Choose appropriate child_process method"," 3. Use argument arrays instead of string interpolation"," 4. Add comprehensive input validation"," 5. Test with malicious inputs"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectChildProcess=(0,eslint_devkit_2.createRule)({name:"detect-child-process",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-child-process.md",description:"Detects child_process usage that may allow command injection",cwe:"CWE-78",cvss:9.8,confidence:"medium"},hasSuggestions:true,messages:{childProcessCommandInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Command injection",cwe:"CWE-78",description:"Command injection detected",severity:"CRITICAL",fix:"Use execFile/spawn with {shell: false} and array args",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),argumentInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Argument injection",cwe:"CWE-88",description:"An attacker-steered value sits in the argv vector with no `--` before it. There is no shell here, but the callee still parses a leading `-` as an option \u2014 `--upload-pack=` (git), `--to-command=` (tar), `-o ProxyCommand=` (ssh) all execute arbitrary programs.",severity:"HIGH",fix:"Insert a literal '--' before the first attacker-controlled element, or reject values beginning with '-'.",documentationLink:"https://cwe.mitre.org/data/definitions/88.html"}),useEndOfOptions:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use end-of-options",description:"Add a literal '--' before user-controlled arguments",severity:"LOW",fix:"execFile('git', ['ls-remote', '--', remote])",documentationLink:"https://cwe.mitre.org/data/definitions/88.html"}),useExecFile:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use execFile",description:"Use execFile() with argument array",severity:"LOW",fix:"execFile(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback"}),useSpawn:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use spawn",description:"Use spawn() with separate arguments",severity:"LOW",fix:"spawn(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processspawncommand-args-options"}),useSaferLibrary:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safer Library",description:"Consider safer command execution libraries",severity:"LOW",fix:"Use execa, zx, or cross-spawn instead",documentationLink:"https://github.com/sindresorhus/execa"}),validateInput:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Input",description:"Add input validation and sanitization",severity:"LOW",fix:"Validate user input before passing to command",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),useShellFalse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Disable Shell",description:"Use shell: false option",severity:"LOW",fix:"{ shell: false } to prevent shell interpretation",documentationLink:"https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Validate Strategy",description:"Comprehensive input validation",severity:"LOW",fix:"Add allowlist validation before command execution",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategySanitize:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Sanitize Strategy",description:"Sanitize and escape command arguments",severity:"LOW",fix:"Escape special characters in command arguments",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategyRestrict:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Restrict Strategy",description:"Restrict to predefined safe commands",severity:"LOW",fix:"Define allowlist of permitted commands",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow exec() with literal strings"},allowLiteralSpawn:{type:"boolean",default:false,description:"Allow spawn() with literal arguments"},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional child_process methods to check"},strategy:{type:"string",enum:["validate","sanitize","restrict","auto"],default:"auto",description:"Strategy for fixing command injection (auto = smart detection)"},taintSources:{type:"array",items:{type:"string"},default:DEFAULT_TAINT_SOURCES,description:"Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process)"},reportUnresolvedCommands:{type:"boolean",default:false,description:'Report a command whose provenance cannot be resolved. Restores the pre-inversion "any dynamic argument is dangerous" behaviour.'}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,allowLiteralSpawn:false,additionalMethods:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,allowLiteralSpawn=false,additionalMethods=[]}=options;const taintRoots=new Set((options.taintSources??DEFAULT_TAINT_SOURCES).map(source=>source.toLowerCase()));const readsTaintSource=(0,provenance_1.makeReadsTaintSource)(context.sourceCode,taintRoots);const readsRemoteTaintSource=(0,provenance_1.makeReadsTaintSource)(context.sourceCode,new Set([...taintRoots].filter(root=>root!==LOCAL_TAINT_ROOT)));const reportUnresolvedCommands=options.reportUnresolvedCommands??false;const dangerousMethodsSet=new Set(["exec","execSync","execFile","execFileSync","spawn","spawnSync","fork","forkSync",...additionalMethods]);const isChildProcessSpecifier=value=>value==="child_process"||value==="node:child_process";const moduleAliases=new Set(["child_process"]);const importedMethods=new Set;const containsDynamicStrings=node=>!(0,eslint_devkit_2.isStaticExpression)({node,scope:context.sourceCode.getScope(node)});const isFreeReference=node=>{const name=node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node:node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node.callee:null;if(!name)return false;return context.sourceCode.getScope(name).through.some(ref=>ref.identifier===name&&ref.resolved===null)};const isStaticArg=argument=>(0,eslint_devkit_2.isStaticExpression)({node:argument,scope:context.sourceCode.getScope(argument)});const hasOnlyLiteralArgs=args=>{if(args.length===0)return false;if(!isStaticArg(args[0])){return false}if(args.length>=2){const argsArray=args[1];if(argsArray.type==="ArrayExpression"){const allLiteralElements=argsArray.elements.every(el=>el!==null&&isStaticArg(el));if(!allLiteralElements){return false}}else if(!isStaticArg(argsArray)){return false}}return true};const usesShell=(node,method)=>{if(method==="exec"||method==="execSync")return true;const command=node.arguments[0];if(command!==void 0&&command.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof command.value==="string"&&SHELL_BINARIES.has(command.value.replace(/^.*[/\\]/,"").toLowerCase())){return true}const argv=node.arguments[1];if(argv?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){for(const element of argv.elements){if(element?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof element.value==="string"&&EVAL_FLAGS.has(element.value.toLowerCase())){return true}}}for(const candidate of[node.arguments[1],node.arguments[2]]){if(!candidate||candidate.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)continue;for(const property of candidate.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.key.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||property.key.name!=="shell"){continue}return!(property.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value.value===false)}}return false};const cannotStartWithDash=node=>{if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){const first=node.quasis[0].value.raw;return first.length>0&&!first.startsWith("-")}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"){const left=node.left;return left.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof left.value==="string"&&left.value.length>0&&!left.value.startsWith("-")}return false};const argumentInjectionSite=node=>{const argv=node.arguments[1];if(argv?.type!==eslint_devkit_1.AST_NODE_TYPES.ArrayExpression)return null;for(const element of argv.elements){if(element===null)continue;if(element.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&element.value==="--"){return null}if(!readsRemoteTaintSource(element))continue;const target=element.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement?element.argument:element;if(cannotStartWithDash(target))continue;return element}return null};const hasShellFalseOption=node=>{const optionsArg=node.arguments[2];if(!optionsArg||optionsArg.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){return true}for(const prop of optionsArg.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&prop.key.name==="shell"){if(prop.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&prop.value.value===false){return true}return false}}return true};const hasPrecedingAllowlistValidation=node=>{const makeArgChecker=validatedVarNames=>{const check=argNode=>{if(argNode.type==="Identifier"&&validatedVarNames.has(argNode.name))return true;if(argNode.type==="TemplateLiteral"){return argNode.expressions.some(e=>e.type==="Identifier"&&validatedVarNames.has(e.name))}if(argNode.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){return argNode.elements.some(el=>el!==null&&check(el))}return false};return check};const checkGuardClause=ifNode=>{const test=ifNode.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){const validatedVarNames=new Set;for(const testArg of test.arguments){if(testArg.type==="Identifier")validatedVarNames.add(testArg.name)}const check=makeArgChecker(validatedVarNames);for(const arg of node.arguments){if(check(arg))return true}}if(test.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&test.operator==="!"&&test.argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&test.argument.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&test.argument.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&test.argument.callee.property.name==="includes"){const consequent=ifNode.consequent;const isGuardBody=consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&&consequent.body.length>0&&(consequent.body[0].type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement||consequent.body[0].type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement);if(isGuardBody){const validatedVarNames=new Set;for(const testArg of test.argument.arguments){if(testArg.type==="Identifier")validatedVarNames.add(testArg.name)}const check=makeArgChecker(validatedVarNames.size>0?validatedVarNames:new Set(["*"]));if(validatedVarNames.size>0){for(const arg of node.arguments){if(check(arg))return true}}else{for(const arg of node.arguments){if(arg.type==="Identifier"||arg.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression&&arg.elements.some(el=>el?.type==="Identifier")){return true}}}}}return false};let current=node.parent;while(current){if(current.type==="IfStatement"){if(checkGuardClause(current))return true}current=current.parent}let stmt=node.parent;while(stmt&&stmt.parent&&stmt.parent.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement){stmt=stmt.parent}if(stmt&&stmt.parent&&stmt.parent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){const block=stmt.parent;const callIndex=block.body.indexOf(stmt);if(callIndex>0){for(let i=0;i<callIndex;i++){const sibling=block.body[i];if(sibling.type==="IfStatement"){if(checkGuardClause(sibling))return true}}}}return false};const extractCommandInfo=(node,method)=>{const sourceCode=context.sourceCode;const args=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=COMMAND_PATTERNS.find(p=>p.method===method)||null;const injectableArgs=node.arguments.slice(0,1);const argvArray=node.arguments[1];if(argvArray?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){injectableArgs.push(...argvArray.elements.filter(el=>el!==null))}const isDynamic=injectableArgs.some(arg=>containsDynamicStrings(arg));return{args,pattern,isDynamic}};const determineRiskLevel=(pattern,isDynamic)=>{if(pattern?.dangerous&&isDynamic){return"critical"}if(pattern?.dangerous||isDynamic){return"high"}return"medium"};const isChildProcessRequire=node=>node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&isChildProcessSpecifier(node.arguments[0].value);const getChildProcessCall=node=>{if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(!dangerousMethodsSet.has(methodName)){return null}if(node.callee.object.type==="Identifier"&&moduleAliases.has(node.callee.object.name)){return{method:methodName,calleeNode:node.callee}}if(isChildProcessRequire(node.callee.object)){return{method:methodName,calleeNode:node.callee}}}if(node.callee.type==="Identifier"&&dangerousMethodsSet.has(node.callee.name)){if(importedMethods.has(node.callee.name)){return{method:node.callee.name,calleeNode:node.callee}}}return null};const checkChildProcessCall=node=>{const detected=getChildProcessCall(node);if(!detected){return}const{method}=detected;const{args,pattern,isDynamic}=extractCommandInfo(node,method);if((method==="exec"||method==="execSync")&&!isDynamic&&hasOnlyLiteralArgs(node.arguments)){return}if(allowLiteralStrings&&method==="exec"&&!isDynamic){return}const saferMethods=new Set(["spawn","spawnSync","execFile","execFileSync"]);if(allowLiteralSpawn&&saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){return}if(saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){const isExecFile=method==="execFile"||method==="execFileSync";if(isExecFile||hasShellFalseOption(node)){return}}const allSafeMethods=["execFile","execFileSync","spawn","spawnSync"];if(allSafeMethods.includes(method)&&hasPrecedingAllowlistValidation(node)){return}const command=node.arguments[0];const commandIsSteerable=command===void 0||isFreeReference(command)||readsRemoteTaintSource(command);if(!usesShell(node,method)&&!commandIsSteerable){const injected=argumentInjectionSite(node);if(injected===null)return;context.report({node:injected,messageId:"argumentInjection",suggest:[{messageId:"useEndOfOptions",fix:()=>null},{messageId:"validateInput",fix:()=>null}]});return}const injectablePositions=node.arguments.slice(0,1);const argvVector=node.arguments[1];if(argvVector?.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){injectablePositions.push(...argvVector.elements.filter(el=>el!==null))}const unknowable=injectablePositions.some(argument=>isFreeReference(argument));if(!reportUnresolvedCommands&&!unknowable&&!node.arguments.some(argument=>readsTaintSource(argument))){return}const riskLevel=determineRiskLevel(pattern,isDynamic);const steps=pattern?(0,exports.generateRefactoringSteps)(pattern):"Review and secure command execution";const alternatives=pattern?.safeAlternatives.join(", ")||"execFile, spawn with validation";context.report({node,messageId:"childProcessCommandInjection",data:{method,args,riskLevel,vulnerability:pattern?.vulnerability||"command injection",alternatives,steps,effort:pattern?.effort||"15-30 minutes"},suggest:[{messageId:"useExecFile",fix:()=>null},{messageId:"useSpawn",fix:()=>null},{messageId:"useSaferLibrary",fix:()=>null},{messageId:"validateInput",fix:()=>null},{messageId:"useShellFalse",fix:()=>null}]})};const trackChildProcessImport=node=>{if(!isChildProcessSpecifier(node.source.value)){return}for(const specifier of node.specifiers){if(specifier.type==="ImportDefaultSpecifier"||specifier.type==="ImportNamespaceSpecifier"){moduleAliases.add(specifier.local.name)}if(specifier.type==="ImportSpecifier"){importedMethods.add(specifier.local.name)}}};const trackChildProcessRequire=node=>{if(!node.init){return}if(node.id.type==="Identifier"&&node.init.type==="CallExpression"&&node.init.callee.type==="Identifier"&&node.init.callee.name==="require"&&node.init.arguments[0]&&node.init.arguments[0].type==="Literal"&&isChildProcessSpecifier(node.init.arguments[0].value)){moduleAliases.add(node.id.name);return}if(node.id.type==="ObjectPattern"&&node.init?.type==="CallExpression"&&node.init.callee.type==="Identifier"&&node.init.callee.name==="require"&&node.init.arguments[0]&&node.init.arguments[0].type==="Literal"&&isChildProcessSpecifier(node.init.arguments[0].value)){for(const prop of node.id.properties){if(prop.type==="Property"&&prop.key.type==="Identifier"){importedMethods.add(prop.value.type==="Identifier"?prop.value.name:prop.key.name)}}}};const checkBareChildProcessRequire=node=>{if(!isChildProcessRequire(node))return;const parent=node.parent;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.init===node)return;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===node)return;context.report({node,messageId:"childProcessCommandInjection",data:{method:"require",riskLevel:"MEDIUM",vulnerability:"command-injection",safeAlternatives:"execFile, spawn",refactoringSteps:" 1. Avoid importing child_process where it is not needed\n 2. If required, prefer execFile()/spawn() with {shell: false}\n 3. Validate any command or argument that is not a literal",effort:"10-15 minutes",badExample:"require('child_process')",goodExample:"const { execFile } = require('node:child_process')"}})};return{CallExpression(node){checkChildProcessCall(node);checkBareChildProcessRequire(node)},ImportDeclaration:trackChildProcessImport,VariableDeclarator:trackChildProcessRequire}}});
|
|
@@ -1 +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)) 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)) return;\nfs.readdir(resolvedDir, callback)"},effort:"15-20 minutes"}];const hasTraversalPatterns=pathStr=>{return/\.\.[/\\]/.test(pathStr)||/^\.\.[/\\]/.test(pathStr)};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 if resolved path starts with allowed base directory"," 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_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"},hasSuggestions:true,messages:{fsPathTraversal:(0,eslint_devkit_1.formatLLMMessage)({icon:"\u{1F511}",issueName:"Path traversal",cwe:"CWE-22",description:"Path traversal vulnerability",severity:"{{riskLevel}}",fix:"{{safePattern}}",documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"}),usePathResolve:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use path.resolve",description:"Use path.resolve() to normalize paths",severity:"LOW",fix:"path.resolve(SAFE_DIR, userInput)",documentationLink:"https://nodejs.org/api/path.html#pathresolvepaths"}),validatePath:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Path",description:"Validate resolved path starts with allowed base",severity:"LOW",fix:"if (!resolved.startsWith(SAFE_DIR)) throw new Error()",documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"}),useBasename:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use path.basename",description:"Use path.basename() to strip directory components",severity:"LOW",fix:"path.basename(userInput)",documentationLink:"https://nodejs.org/api/path.html#pathbasenamepath-suffix"}),createSafeDir:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Define Safe Directory",description:"Define SAFE_DIR constant",severity:"LOW",fix:'const SAFE_DIR = path.resolve(__dirname, "uploads")',documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"}),whitelistExtensions:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Whitelist Extensions",description:"Whitelist allowed file extensions",severity:"LOW",fix:'const ALLOWED_EXT = [".txt", ".pdf"]; if (!ALLOWED_EXT.includes(ext)) throw',documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"})},schema:[{type:"object",properties:{taintSources:{type:"array",items:{type:"string"},description:"Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process)"},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"},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional fs methods to check"},allowedExtensions:{type:"array",items:{type:"string"},default:[],description:'Allowed file extensions (e.g., [".txt", ".json"])'}},additionalProperties:false}]},defaultOptions:[{allowLiterals:false,additionalMethods:[]}],create(context){const options=context.options[0]||{};const{allowLiterals=false,additionalMethods=[]}=options;const DEFAULT_TAINT_ROOTS=["process"];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","cp","cpSync","rm","rmSync","copyFile","copyFileSync","rename","renameSync","truncate","truncateSync","symlink","symlinkSync","link","linkSync","utimes","utimesSync","chmod","chmodSync","open","openSync","opendir","opendirSync",...additionalMethods]);const isLiteralString=node=>{return node.type==="Literal"&&typeof node.value==="string"};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))return true;const bound=constBindings.get(node.name);return bound!==void 0&&readsTaintSource(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{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:return node.arguments.some(arg=>arg.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsTaintSource(arg,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 true;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;return callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&node.arguments.length===1&&isWholeTaintValue(node.arguments[0])}default:return false}};const isDangerousPath=(pathNode,pathStr)=>{if(!pathNode)return reportUnresolvedPaths;if(isLiteralString(pathNode)){return!allowLiterals&&hasTraversalPatterns(pathStr)}if(hasPathValidation(pathNode))return false;if(readsTaintSource(pathNode))return!isWholeTaintValue(pathNode);if(isBuildTimeConstant(pathNode))return false;return reportUnresolvedPaths};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 hasPathValidation=pathNode=>{if(pathNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return false}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 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==="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=>{const methodName=fsMethodName(node.callee,fsNamespaces,fsNamedMethods);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"},suggest:[{messageId:"validatePath",fix:()=>null},{messageId:"usePathResolve",fix:()=>null},{messageId:"whitelistExtensions",fix:()=>null}]})}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"},suggest:[{messageId:"usePathResolve",fix:()=>null},{messageId:"validatePath",fix:()=>null},{messageId:"useBasename",fix:()=>null},{messageId:"createSafeDir",fix:()=>null},{messageId:"whitelistExtensions",fix:()=>null}]})};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:prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof prop.key.value==="string"?prop.key.value: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)) 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)) return;\nfs.readdir(resolvedDir, callback)"},effort:"15-20 minutes"}];const hasTraversalPatterns=pathStr=>{return/\.\.[/\\]/.test(pathStr)||/^\.\.[/\\]/.test(pathStr)};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 if resolved path starts with allowed base directory"," 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"},hasSuggestions:true,messages:{fsPathTraversal:(0,eslint_devkit_1.formatLLMMessage)({icon:"\u{1F511}",issueName:"Path traversal",cwe:"CWE-22",description:"Path traversal vulnerability",severity:"{{riskLevel}}",fix:"{{safePattern}}",documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"}),usePathResolve:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use path.resolve",description:"Use path.resolve() to normalize paths",severity:"LOW",fix:"path.resolve(SAFE_DIR, userInput)",documentationLink:"https://nodejs.org/api/path.html#pathresolvepaths"}),validatePath:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Path",description:"Validate resolved path starts with allowed base",severity:"LOW",fix:"if (!resolved.startsWith(SAFE_DIR)) throw new Error()",documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"}),useBasename:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use path.basename",description:"Use path.basename() to strip directory components",severity:"LOW",fix:"path.basename(userInput)",documentationLink:"https://nodejs.org/api/path.html#pathbasenamepath-suffix"}),createSafeDir:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Define Safe Directory",description:"Define SAFE_DIR constant",severity:"LOW",fix:'const SAFE_DIR = path.resolve(__dirname, "uploads")',documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"}),whitelistExtensions:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Whitelist Extensions",description:"Whitelist allowed file extensions",severity:"LOW",fix:'const ALLOWED_EXT = [".txt", ".pdf"]; if (!ALLOWED_EXT.includes(ext)) throw',documentationLink:"https://owasp.org/www-community/attacks/Path_Traversal"})},schema:[{type:"object",properties:{taintSources:{type:"array",items:{type:"string"},description:"Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process)"},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"},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional fs methods to check"},allowedExtensions:{type:"array",items:{type:"string"},default:[],description:'Allowed file extensions (e.g., [".txt", ".json"])'}},additionalProperties:false}]},defaultOptions:[{allowLiterals:false,additionalMethods:[]}],create(context){const options=context.options[0]||{};const{allowLiterals=false,additionalMethods=[]}=options;const DEFAULT_TAINT_ROOTS=["process"];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 node.type==="Literal"&&typeof node.value==="string"};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))return true;const bound=constBindings.get(node.name);return bound!==void 0&&readsTaintSource(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{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:return node.arguments.some(arg=>arg.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsTaintSource(arg,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 true;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;return callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&node.arguments.length===1&&isWholeTaintValue(node.arguments[0])}default:return false}};const isDangerousPath=(pathNode,pathStr)=>{if(!pathNode)return reportUnresolvedPaths;if(isLiteralString(pathNode)){return!allowLiterals&&hasTraversalPatterns(pathStr)}if(hasPathValidation(pathNode))return false;if(readsTaintSource(pathNode))return!isWholeTaintValue(pathNode);if(isBuildTimeConstant(pathNode))return false;if(isFreeVariable(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 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 hasPathValidation=pathNode=>{if(pathNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return false}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 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==="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"},suggest:[{messageId:"validatePath",fix:()=>null},{messageId:"usePathResolve",fix:()=>null},{messageId:"whitelistExtensions",fix:()=>null}]})}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"},suggest:[{messageId:"usePathResolve",fix:()=>null},{messageId:"validatePath",fix:()=>null},{messageId:"useBasename",fix:()=>null},{messageId:"createSafeDir",fix:()=>null},{messageId:"whitelistExtensions",fix:()=>null}]})};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:prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof prop.key.value==="string"?prop.key.value: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)}}}});
|
package/src/utils/provenance.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.findVariable=findVariable;exports.bindingInit=bindingInit;exports.constLiteralOf=constLiteralOf;exports.makeReadsTaintSource=makeReadsTaintSource;const eslint_devkit_1=require("@interlace/eslint-devkit");function findVariable(sourceCode,node){let scope=sourceCode.getScope(node);while(scope){const found=scope.set?.get(node.name);if(found)return found;scope=scope.upper}return null}function bindingInit(sourceCode,node){const variable=findVariable(sourceCode,node);if(!variable||variable.defs.length!==1)return void 0;const def=variable.defs[0];if(def.type!=="Variable")return void 0;return def.node.init??void 0}function constLiteralOf(sourceCode,node){const variable=findVariable(sourceCode,node);if(!variable||variable.defs.length!==1)return void 0;const def=variable.defs[0];if(def.type!=="Variable")return void 0;if(def.parent.kind!=="const")return void 0;if(def.node.id.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return void 0;const init=def.node.init;if(!init)return void 0;if(init.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return init;if(init.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral&&init.expressions.length===0){return init}return void 0}const REQUEST_PROPERTY_NAMES=new Set(["headers","query","body","params","cookies","searchparams","rawbody","querystringparameters","pathparameters","formdata"]);function makeReadsTaintSource(sourceCode,roots){const reads=(node,depth)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(roots.has(node.name.toLowerCase()))return true;const init=bindingInit(sourceCode,node);
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.findVariable=findVariable;exports.bindingInit=bindingInit;exports.constLiteralOf=constLiteralOf;exports.makeReadsTaintSource=makeReadsTaintSource;const eslint_devkit_1=require("@interlace/eslint-devkit");function findVariable(sourceCode,node){let scope=sourceCode.getScope(node);while(scope){const found=scope.set?.get(node.name);if(found)return found;scope=scope.upper}return null}function bindingInit(sourceCode,node){const variable=findVariable(sourceCode,node);if(!variable||variable.defs.length!==1)return void 0;const def=variable.defs[0];if(def.type!=="Variable")return void 0;return def.node.init??void 0}function constLiteralOf(sourceCode,node){const variable=findVariable(sourceCode,node);if(!variable||variable.defs.length!==1)return void 0;const def=variable.defs[0];if(def.type!=="Variable")return void 0;if(def.parent.kind!=="const")return void 0;if(def.node.id.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return void 0;const init=def.node.init;if(!init)return void 0;if(init.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return init;if(init.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral&&init.expressions.length===0){return init}return void 0}const REQUEST_PROPERTY_NAMES=new Set(["headers","query","body","params","cookies","searchparams","rawbody","querystringparameters","pathparameters","formdata"]);function makeReadsTaintSource(sourceCode,roots){const reads=(node,depth)=>{if(depth>6)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{if(roots.has(node.name.toLowerCase()))return true;const init=bindingInit(sourceCode,node);if(init!==void 0&&reads(init,depth+1))return true;const variable=findVariable(sourceCode,node);const priorWrites=(variable?.references??[]).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]);const lastWrite=priorWrites.at(-1);return lastWrite!==void 0&&reads(lastWrite,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if(!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&REQUEST_PROPERTY_NAMES.has(node.property.name.toLowerCase())){return true}return reads(node.object,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>reads(expression,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return reads(node.left,depth+1)||reads(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.AwaitExpression:return reads(node.argument,depth+1);case eslint_devkit_1.AST_NODE_TYPES.ArrayExpression:return node.elements.some(element=>element!==null&&reads(element,depth+1));case eslint_devkit_1.AST_NODE_TYPES.SpreadElement:return reads(node.argument,depth+1);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(argument=>reads(argument,depth+1))||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!(node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.object.name==="process")&&reads(node.callee.object,depth+1);default:return false}};return node=>reads(node,0)}
|