eslint-plugin-node-security 4.8.1 → 4.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,7 +62,7 @@ npm install eslint-plugin-node-security --save-dev
62
62
  ## 📦 Compatibility
63
63
  | Package | Version |
64
64
  | :--- | :--- |
65
- | ESLint | `^8.0.0 \|\| ^9.0.0 \|\| ^10.0.0` |
65
+ | ESLint | `^8.40.0 \|\| ^9.0.0 \|\| ^10.0.0` |
66
66
  | Node.js | `>=18.0.0` |
67
67
 
68
68
  See the [ESLint Version Support Policy](../../docs/ESLINT_VERSION_SUPPORT.md) — current ecosystem share data, the 20% gate, and the forward-looking exception that covers v10.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-node-security",
3
- "version": "4.8.1",
3
+ "version": "4.9.1",
4
4
  "description": "ESLint plugin for Node.js security — detects command injection, path traversal, SSRF, zip slip, and weak crypto (MD5/SHA-1, ECB, static IV) in fs, child_process, vm, and crypto.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -78,10 +78,10 @@
78
78
  "node": ">=18.0.0"
79
79
  },
80
80
  "dependencies": {
81
- "@interlace/eslint-devkit": "^1.10.0"
81
+ "@interlace/eslint-devkit": "^1.13.0"
82
82
  },
83
83
  "peerDependencies": {
84
- "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
84
+ "eslint": "^8.40.0 || ^9.0.0 || ^10.0.0"
85
85
  },
86
86
  "funding": {
87
87
  "type": "github",
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-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}};exports.plugin={meta:{name:"eslint-plugin-node-security",version:"4.8.1"},rules:exports.rules};const recommendedRules={"node-security/detect-child-process":"error","node-security/detect-eval-with-expression":"error","node-security/detect-non-literal-fs-filename":"warn","node-security/no-unsafe-dynamic-require":"error","node-security/no-buffer-overread":"warn","node-security/no-deprecated-buffer":"error","node-security/no-unsafe-buffer-alloc":"warn","node-security/no-toctou-vulnerability":"error","node-security/no-zip-slip":"error","node-security/no-arbitrary-file-access":"error","node-security/no-data-in-temp-storage":"error","node-security/no-ssrf":"warn","node-security/no-shell-injection":"error","node-security/no-dynamic-command-string":"error","node-security/no-dynamic-algorithm-selection":"error","node-security/no-timing-unsafe-compare":"warn","node-security/detect-suspicious-dependencies":"warn","node-security/lock-file":"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"};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
+ "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-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}};exports.plugin={meta:{name:"eslint-plugin-node-security",version:"4.9.1"},rules:exports.rules};const recommendedRules={"node-security/detect-child-process":"error","node-security/detect-eval-with-expression":"error","node-security/detect-non-literal-fs-filename":"warn","node-security/no-unsafe-dynamic-require":"error","node-security/no-buffer-overread":"warn","node-security/no-deprecated-buffer":"error","node-security/no-unsafe-buffer-alloc":"warn","node-security/no-toctou-vulnerability":"error","node-security/no-zip-slip":"error","node-security/no-arbitrary-file-access":"error","node-security/no-data-in-temp-storage":"error","node-security/no-ssrf":"warn","node-security/no-shell-injection":"error","node-security/no-dynamic-command-string":"error","node-security/no-dynamic-algorithm-selection":"error","node-security/no-timing-unsafe-compare":"warn","node-security/detect-suspicious-dependencies":"warn","node-security/lock-file":"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"};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 COMMAND_PATTERNS=[{method:"exec",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFile","spawn"],example:{bad:"exec(`git clone ${repoUrl}`)",good:["execFile('git', ['clone', repoUrl], {shell: false})","spawn('git', ['clone', repoUrl], {shell: false})"]},effort:"15-25 minutes"},{method:"execSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFileSync","spawnSync"],example:{bad:"execSync(`npm install ${packageName}`)",good:["execFileSync('npm', ['install', packageName], {shell: false})","spawnSync('npm', ['install', packageName], {shell: false})"]},effort:"15-25 minutes"},{method:"spawn",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawn with validation"],example:{bad:"spawn('bash', ['-c', userCommand])",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"20-30 minutes"},{method:"execFile",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"execFile(userCommand, userArgs, callback)",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"execFileSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"execFileSync(userCommand, userArgs)",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"spawnSync",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawnSync with validation"],example:{bad:"spawnSync('bash', ['-c', userCommand])",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"15-20 minutes"},{method:"fork",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"fork(userScript)",good:["spawn('node', [validatedScript], {shell: false})","// Validate script path first"]},effort:"15-20 minutes"},{method:"forkSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"forkSync(userScript)",good:["spawnSync('node', [validatedScript], {shell: false, stdio: 'inherit'})","// Validate script path first"]},effort:"15-20 minutes"}];const generateRefactoringSteps=pattern=>{switch(pattern.method){case"exec":case"execSync":return[" 1. Replace exec() with execFile() or spawn()"," 2. Split command and arguments into separate array elements"," 3. Use {shell: false} option to prevent shell interpretation"," 4. Validate and sanitize all user inputs"," 5. Consider using execa library for better security"].join("\n");case"spawn":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Consider using cross-spawn for cross-platform safety"].join("\n");case"execFile":return[" 1. Replace execFile() with spawn() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"execFileSync":return[" 1. Replace execFileSync() with spawnSync() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"spawnSync":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Handle synchronous execution properly"].join("\n");case"fork":return[" 1. Replace fork() with spawn() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawn('node', [scriptPath], options) instead"," 4. Add proper error handling"," 5. Consider using child_process.execFile() for simple scripts"].join("\n");case"forkSync":return[" 1. Replace forkSync() with spawnSync() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawnSync('node', [scriptPath], options) instead"," 4. Add proper error handling and synchronous waiting"," 5. Consider using child_process.execFileSync() for simple scripts"].join("\n");default:return[" 1. Identify the specific command execution need"," 2. Choose appropriate child_process method"," 3. Use argument arrays instead of string interpolation"," 4. Add comprehensive input validation"," 5. Test with malicious inputs"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectChildProcess=(0,eslint_devkit_2.createRule)({name:"detect-child-process",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-child-process.md",description:"Detects child_process usage that may allow command injection",cwe:"CWE-78",cvss:9.8,confidence:"medium"},messages:{childProcessCommandInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Command injection",cwe:"CWE-78",description:"Command injection detected",severity:"CRITICAL",fix:"Use execFile/spawn with {shell: false} and array args",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),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)"}},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 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 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}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 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"}),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)"}},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 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 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}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 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectEvalWithExpression=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const EVAL_PATTERNS=[{pattern:"JSON\\.parse|parse\\(.*\\)",category:"json",safeAlternative:"JSON.parse()",example:{bad:`eval('{"key": "' + value + '"}"')`,good:`JSON.parse('{"key": "' + value + '"}"')`},effort:"2 minutes"},{pattern:"Math\\.|parseInt|parseFloat",category:"math",safeAlternative:"Math functions or parseInt/parseFloat",example:{bad:"eval('Math.' + method + '(' + arg + ')')",good:"const mathMethods = {sin: Math.sin, cos: Math.cos}; mathMethods[method](arg)"},effort:"5 minutes"},{pattern:"\\$\\{|template|interpolat",category:"template",safeAlternative:"Template literals or template engine",example:{bad:"eval('Hello ' + userName + '!')",good:"const template = `Hello ${userName}!`;"},effort:"3 minutes"},{pattern:"\\[.*\\]|object\\[|obj\\.|\\.",category:"object",safeAlternative:"Direct property access or Map",example:{bad:"eval('obj.' + property)",good:"const allowedProps = {name: true, age: true}; if (allowedProps[property]) obj[property]"},effort:"8 minutes"}];const generateRefactoringSteps=pattern=>{if(!pattern){return[" 1. Remove eval() usage entirely"," 2. Identify what the code is trying to achieve"," 3. Use appropriate safe alternative (JSON.parse, Map, etc.)"," 4. Add input validation if dynamic behavior needed"," 5. Test thoroughly for edge cases"].join("\n")}switch(pattern.category){case"json":return[" 1. Replace eval() with JSON.parse()"," 2. Ensure input is valid JSON string"," 3. Add try/catch for JSON parsing errors"," 4. Consider using a JSON schema validator"].join("\n");case"math":return[" 1. Create whitelist of allowed Math functions"," 2. Use direct function calls: Math.sin(x)"," 3. Validate inputs are numbers"," 4. Consider using a math expression parser library"].join("\n");case"template":return[" 1. Use template literals: `Hello ${name}`"," 2. Sanitize variables before interpolation"," 3. Use a template engine like Handlebars if complex"," 4. Validate template structure"].join("\n");case"object":return[" 1. Use Map or plain object for key-value access"," 2. Whitelist allowed property names"," 3. Use hasOwnProperty() check"," 4. Consider Object.create(null) for clean objects"].join("\n");default:return[" 1. Identify the specific use case"," 2. Find a safer alternative approach"," 3. Add comprehensive input validation"," 4. Use static analysis if possible"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectEvalWithExpression=(0,eslint_devkit_2.createRule)({name:"detect-eval-with-expression",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-eval-with-expression.md",description:"Detects eval(variable) which can allow an attacker to run arbitrary code",cwe:"CWE-95",cvss:9.8,confidence:"high"},messages:{evalWithExpression:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() with dynamic code",cwe:"CWE-95",description:"eval() with dynamic code",severity:"CRITICAL",fix:"{{safeAlternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useJsonParse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for JSON parsing",cwe:"CWE-95",description:"Use JSON.parse() instead of eval() for JSON string parsing",severity:"HIGH",fix:"Replace eval() with JSON.parse()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useObjectAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for property access",cwe:"CWE-95",description:"Use direct property access instead of eval() for dynamic property access",severity:"HIGH",fix:"Use obj[key] or Map.get(key) instead of eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useTemplateLiteral:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for string interpolation",cwe:"CWE-95",description:"Use template literals instead of eval() for string interpolation",severity:"HIGH",fix:"Replace eval() with template literals: `Hello ${name}`",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useFunctionConstructor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for function creation",cwe:"CWE-95",description:"Use Function constructor with validation instead of eval()",severity:"HIGH",fix:"Replace eval() with validated Function constructor",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useSaferAlternative:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() usage detected",cwe:"CWE-95",description:"eval() with dynamic code execution detected",severity:"HIGH",fix:"{{alternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRemove:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Critical eval() security vulnerability",cwe:"CWE-95",description:"eval() usage poses severe security risk",severity:"CRITICAL",fix:"Remove eval() entirely - security risk too high",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRefactor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() refactoring required",cwe:"CWE-95",description:"eval() can be refactored to safer alternative",severity:"HIGH",fix:"{{safeAlternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() input validation needed",cwe:"CWE-95",description:"eval() requires input validation for security",severity:"MEDIUM",fix:"Add input validation before using eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow eval with literal strings (false = stricter)"},additionalEvalFunctions:{type:"array",items:{type:"string"},default:[],description:"Additional functions to treat as eval-like"},strategy:{type:"string",enum:["remove","refactor","validate","auto"],default:"auto",description:"Strategy for fixing eval usage (auto = smart detection)"}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,additionalEvalFunctions:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,additionalEvalFunctions=[],strategy="auto"}=options;const evalFunctions=new Set(["eval","Function",...additionalEvalFunctions]);const isLiteralString=node=>{return node.type==="Literal"&&typeof node.value==="string"};const selectStrategyMessage=pattern=>{switch(strategy){case"remove":return"strategyRemove";case"refactor":return"strategyRefactor";case"validate":return"strategyValidate";case"auto":default:if(pattern&&pattern.category==="json"){return"useJsonParse"}if(pattern&&pattern.category==="object"){return"useObjectAccess"}if(pattern&&pattern.category==="template"){return"useTemplateLiteral"}return"strategyRefactor"}};const detectPattern=expression=>{for(const pattern of EVAL_PATTERNS){if(new RegExp(pattern.pattern,"i").test(expression)){return pattern}}return null};const extractExpression=node=>{const sourceCode=context.sourceCode;if(node.arguments.length>0){return sourceCode.getText(node.arguments[0])}return"dynamic expression"};const checkCallExpression=node=>{if(node.callee.type==="Identifier"&&evalFunctions.has(node.callee.name)){if(allowLiteralStrings&&node.arguments.length>0&&isLiteralString(node.arguments[0])){return}if(node.arguments.length>0&&node.callee.name==="eval"&&isLiteralString(node.arguments[0])){return}const expression=extractExpression(node);const pattern=detectPattern(expression);const steps=(0,exports.generateRefactoringSteps)(pattern);const strategyMessageId=selectStrategyMessage(pattern);context.report({node,messageId:strategyMessageId,data:{expression,patternCategory:pattern?.category||"dynamic code execution",safeAlternative:pattern?.safeAlternative||"Remove eval entirely",steps,effort:pattern?.effort||"15-30 minutes"},suggest:pattern?[{messageId:strategyMessageId,data:{safeAlternative:pattern.safeAlternative,alternative:pattern.safeAlternative},fix:()=>null}]:void 0})}if(node.callee.type==="NewExpression"&&node.callee.callee.type==="Identifier"&&node.callee.callee.name==="Function"){const expression=extractExpression(node);const pattern=detectPattern(expression);const strategyMessageId=selectStrategyMessage(pattern);context.report({node,messageId:strategyMessageId,data:{expression:`new Function(${expression})`,patternCategory:"function constructor",safeAlternative:"Arrow function or regular function",steps:[" 1. Replace Function constructor with arrow function"," 2. Use regular function declaration"," 3. Validate any dynamic parts"," 4. Consider module imports instead"].join("\n"),effort:"10 minutes"}})}};const checkNewExpression=node=>{if(node.callee.type==="Identifier"&&node.callee.name==="Function"){const sourceCode=context.sourceCode;const expression=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=detectPattern(expression);const strategyMessageId=selectStrategyMessage(pattern);context.report({node,messageId:strategyMessageId,data:{expression:`new Function(${expression})`,patternCategory:"function constructor",safeAlternative:"Arrow function or regular function",steps:[" 1. Replace Function constructor with arrow function"," 2. Use regular function declaration"," 3. Validate any dynamic parts"," 4. Consider module imports instead"].join("\n"),effort:"10 minutes"}})}};return{CallExpression:checkCallExpression,NewExpression:checkNewExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectEvalWithExpression=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const EVAL_PATTERNS=[{pattern:"JSON\\.parse|parse\\(.*\\)",category:"json",safeAlternative:"JSON.parse()",example:{bad:`eval('{"key": "' + value + '"}"')`,good:`JSON.parse('{"key": "' + value + '"}"')`},effort:"2 minutes"},{pattern:"Math\\.|parseInt|parseFloat",category:"math",safeAlternative:"Math functions or parseInt/parseFloat",example:{bad:"eval('Math.' + method + '(' + arg + ')')",good:"const mathMethods = {sin: Math.sin, cos: Math.cos}; mathMethods[method](arg)"},effort:"5 minutes"},{pattern:"\\$\\{|template|interpolat",category:"template",safeAlternative:"Template literals or template engine",example:{bad:"eval('Hello ' + userName + '!')",good:"const template = `Hello ${userName}!`;"},effort:"3 minutes"},{pattern:"\\[.*\\]|object\\[|obj\\.|\\.",category:"object",safeAlternative:"Direct property access or Map",example:{bad:"eval('obj.' + property)",good:"const allowedProps = {name: true, age: true}; if (allowedProps[property]) obj[property]"},effort:"8 minutes"}];const generateRefactoringSteps=pattern=>{if(!pattern){return[" 1. Remove eval() usage entirely"," 2. Identify what the code is trying to achieve"," 3. Use appropriate safe alternative (JSON.parse, Map, etc.)"," 4. Add input validation if dynamic behavior needed"," 5. Test thoroughly for edge cases"].join("\n")}switch(pattern.category){case"json":return[" 1. Replace eval() with JSON.parse()"," 2. Ensure input is valid JSON string"," 3. Add try/catch for JSON parsing errors"," 4. Consider using a JSON schema validator"].join("\n");case"math":return[" 1. Create whitelist of allowed Math functions"," 2. Use direct function calls: Math.sin(x)"," 3. Validate inputs are numbers"," 4. Consider using a math expression parser library"].join("\n");case"template":return[" 1. Use template literals: `Hello ${name}`"," 2. Sanitize variables before interpolation"," 3. Use a template engine like Handlebars if complex"," 4. Validate template structure"].join("\n");case"object":return[" 1. Use Map or plain object for key-value access"," 2. Whitelist allowed property names"," 3. Use hasOwnProperty() check"," 4. Consider Object.create(null) for clean objects"].join("\n");default:return[" 1. Identify the specific use case"," 2. Find a safer alternative approach"," 3. Add comprehensive input validation"," 4. Use static analysis if possible"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectEvalWithExpression=(0,eslint_devkit_2.createRule)({name:"detect-eval-with-expression",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-eval-with-expression.md",description:"Detects eval(variable) which can allow an attacker to run arbitrary code",cwe:"CWE-95",cvss:9.8,confidence:"high"},hasSuggestions:true,messages:{evalWithExpression:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() with dynamic code",cwe:"CWE-95",description:"eval() with dynamic code",severity:"CRITICAL",fix:"{{safeAlternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useJsonParse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for JSON parsing",cwe:"CWE-95",description:"Use JSON.parse() instead of eval() for JSON string parsing",severity:"HIGH",fix:"Replace eval() with JSON.parse()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useObjectAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for property access",cwe:"CWE-95",description:"Use direct property access instead of eval() for dynamic property access",severity:"HIGH",fix:"Use obj[key] or Map.get(key) instead of eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useTemplateLiteral:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for string interpolation",cwe:"CWE-95",description:"Use template literals instead of eval() for string interpolation",severity:"HIGH",fix:"Replace eval() with template literals: `Hello ${name}`",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useFunctionConstructor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() for function creation",cwe:"CWE-95",description:"Use Function constructor with validation instead of eval()",severity:"HIGH",fix:"Replace eval() with validated Function constructor",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),useSaferAlternative:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe eval() usage detected",cwe:"CWE-95",description:"eval() with dynamic code execution detected",severity:"HIGH",fix:"{{alternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRemove:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Critical eval() security vulnerability",cwe:"CWE-95",description:"eval() usage poses severe security risk",severity:"CRITICAL",fix:"Remove eval() entirely - security risk too high",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyRefactor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() refactoring required",cwe:"CWE-95",description:"eval() can be refactored to safer alternative",severity:"HIGH",fix:"{{safeAlternative}}",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"eval() input validation needed",cwe:"CWE-95",description:"eval() requires input validation for security",severity:"MEDIUM",fix:"Add input validation before using eval()",documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow eval with literal strings (false = stricter)"},additionalEvalFunctions:{type:"array",items:{type:"string"},default:[],description:"Additional functions to treat as eval-like"},strategy:{type:"string",enum:["remove","refactor","validate","auto"],default:"auto",description:"Strategy for fixing eval usage (auto = smart detection)"}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,additionalEvalFunctions:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,additionalEvalFunctions=[],strategy="auto"}=options;const evalFunctions=new Set(["eval","Function",...additionalEvalFunctions]);const isLiteralString=node=>{return node.type==="Literal"&&typeof node.value==="string"};const selectStrategyMessage=pattern=>{switch(strategy){case"remove":return"strategyRemove";case"refactor":return"strategyRefactor";case"validate":return"strategyValidate";case"auto":default:if(pattern&&pattern.category==="json"){return"useJsonParse"}if(pattern&&pattern.category==="object"){return"useObjectAccess"}if(pattern&&pattern.category==="template"){return"useTemplateLiteral"}return"strategyRefactor"}};const detectPattern=expression=>{for(const pattern of EVAL_PATTERNS){if(new RegExp(pattern.pattern,"i").test(expression)){return pattern}}return null};const extractExpression=node=>{const sourceCode=context.sourceCode;if(node.arguments.length>0){return sourceCode.getText(node.arguments[0])}return"dynamic expression"};const checkCallExpression=node=>{if(node.callee.type==="Identifier"&&evalFunctions.has(node.callee.name)){if(allowLiteralStrings&&node.arguments.length>0&&isLiteralString(node.arguments[0])){return}if(node.arguments.length>0&&node.callee.name==="eval"&&isLiteralString(node.arguments[0])){return}const expression=extractExpression(node);const pattern=detectPattern(expression);const steps=(0,exports.generateRefactoringSteps)(pattern);const strategyMessageId=selectStrategyMessage(pattern);context.report({node,messageId:strategyMessageId,data:{expression,patternCategory:pattern?.category||"dynamic code execution",safeAlternative:pattern?.safeAlternative||"Remove eval entirely",steps,effort:pattern?.effort||"15-30 minutes"},suggest:pattern?[{messageId:strategyMessageId,data:{safeAlternative:pattern.safeAlternative,alternative:pattern.safeAlternative},fix:()=>null}]:void 0})}if(node.callee.type==="NewExpression"&&node.callee.callee.type==="Identifier"&&node.callee.callee.name==="Function"){const expression=extractExpression(node);const pattern=detectPattern(expression);const strategyMessageId=selectStrategyMessage(pattern);context.report({node,messageId:strategyMessageId,data:{expression:`new Function(${expression})`,patternCategory:"function constructor",safeAlternative:"Arrow function or regular function",steps:[" 1. Replace Function constructor with arrow function"," 2. Use regular function declaration"," 3. Validate any dynamic parts"," 4. Consider module imports instead"].join("\n"),effort:"10 minutes"}})}};const checkNewExpression=node=>{if(node.callee.type==="Identifier"&&node.callee.name==="Function"){const sourceCode=context.sourceCode;const expression=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=detectPattern(expression);const strategyMessageId=selectStrategyMessage(pattern);context.report({node,messageId:strategyMessageId,data:{expression:`new Function(${expression})`,patternCategory:"function constructor",safeAlternative:"Arrow function or regular function",steps:[" 1. Replace Function constructor with arrow function"," 2. Use regular function declaration"," 3. Validate any dynamic parts"," 4. Consider module imports instead"].join("\n"),effort:"10 minutes"}})}};return{CallExpression:checkCallExpression,NewExpression:checkNewExpression}}});
@@ -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"]);const isFsModule=source=>typeof source==="string"&&FS_MODULES.has(source);exports.isFsModule=isFsModule;function fsMethodName(callee,namespaces,named){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return named.get(callee.name);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return void 0}const object=callee.object;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.name)){return callee.property.name}if(object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!object.computed&&object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&namespaces.has(object.object.name)&&object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.property.name==="promises"){return callee.property.name}return void 0}function isFsRequire(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments.length>0&&node.arguments[0].type===eslint_devkit_1.AST_NODE_TYPES.Literal&&(0,exports.isFsModule)(node.arguments[0].value)}const determineRiskLevel=(operation,pathStr)=>{if(hasTraversalPatterns(pathStr)){return"CRITICAL"}if(operation.dangerous){return"HIGH"}return"MEDIUM"};exports.determineRiskLevel=determineRiskLevel;exports.detectNonLiteralFsFilename=(0,eslint_devkit_2.createRule)({name:"detect-non-literal-fs-filename",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-non-literal-fs-filename.md",description:"Detects variable in filename argument of fs calls, which might allow an attacker to access anything on your system",cwe:"CWE-22",confidence:"medium"},messages:{fsPathTraversal:(0,eslint_devkit_1.formatLLMMessage)({icon:"\u{1F511}",issueName:"Path traversal",cwe:"CWE-22",description:"Path traversal vulnerability",severity:"{{riskLevel}}",fix:"{{safePattern}}",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:{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 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",...additionalMethods]);const isLiteralString=node=>{return node.type==="Literal"&&typeof node.value==="string"};const extractPathArgument=(node,method)=>{const operation=FS_OPERATIONS.find(op=>op.method===method)||null;const pathNode=node.arguments.length>0?node.arguments[0]:null;const sourceCode=context.sourceCode;const path=pathNode?sourceCode.getText(pathNode):"";return{path,pathNode,operation}};const isDangerousPath=(pathNode,pathStr)=>{if(allowLiterals&&pathNode&&isLiteralString(pathNode)){return false}if(pathNode&&isLiteralString(pathNode)&&hasTraversalPatterns(pathStr)){return true}if(pathNode&&isSafePathConstruction(pathNode)){return false}if(pathNode&&hasPathValidation(pathNode)){return false}if(pathNode&&pathNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=pathNode.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["join","resolve"].includes(callee.property.name)){const dynamicArgs=pathNode.arguments.filter(arg=>arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name!=="__dirname");if(dynamicArgs.length>0&&dynamicArgs.every(arg=>hasPathValidation(arg))){return false}}}return!pathNode||!isLiteralString(pathNode)};const isSafePathConstruction=pathNode=>{if(pathNode.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression){return false}const callee=pathNode.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||callee.object.name!=="path"||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return false}const method=callee.property.name;if(!["join","resolve"].includes(method)){return false}const args=pathNode.arguments;if(args.length===0){return false}const firstArg=args[0];const isFirstArgSafe=firstArg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&firstArg.name==="__dirname"||firstArg.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof firstArg.value==="string";if(!isFirstArgSafe){return false}for(let i=1;i<args.length;i++){const arg=args[i];if(arg.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof arg.value!=="string"){return false}if(hasTraversalPatterns(String(arg.value))){return false}}return true};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&&current.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=[];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{path,pathNode,operation}=extractPathArgument(node,methodName);const method=methodName;if(!isDangerousPath(pathNode,path)){return}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||!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_MODULES=new Set(["fs","node:fs","fs/promises","node:fs/promises"]);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:{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 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",...additionalMethods]);const isLiteralString=node=>{return node.type==="Literal"&&typeof node.value==="string"};const extractPathArgument=(node,method)=>{const operation=FS_OPERATIONS.find(op=>op.method===method)||null;const pathNode=node.arguments.length>0?node.arguments[0]:null;const sourceCode=context.sourceCode;const path=pathNode?sourceCode.getText(pathNode):"";return{path,pathNode,operation}};const isDangerousPath=(pathNode,pathStr)=>{if(allowLiterals&&pathNode&&isLiteralString(pathNode)){return false}if(pathNode&&isLiteralString(pathNode)&&hasTraversalPatterns(pathStr)){return true}if(pathNode&&isSafePathConstruction(pathNode)){return false}if(pathNode&&hasPathValidation(pathNode)){return false}if(pathNode&&pathNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=pathNode.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="path"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["join","resolve"].includes(callee.property.name)){const dynamicArgs=pathNode.arguments.filter(arg=>arg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&arg.name!=="__dirname");if(dynamicArgs.length>0&&dynamicArgs.every(arg=>hasPathValidation(arg))){return false}}}return!pathNode||!isLiteralString(pathNode)};const isSafePathConstruction=pathNode=>{if(pathNode.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression){return false}const callee=pathNode.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||callee.object.name!=="path"||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return false}const method=callee.property.name;if(!["join","resolve"].includes(method)){return false}const args=pathNode.arguments;if(args.length===0){return false}const firstArg=args[0];const isFirstArgSafe=firstArg.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&firstArg.name==="__dirname"||firstArg.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof firstArg.value==="string";if(!isFirstArgSafe){return false}for(let i=1;i<args.length;i++){const arg=args[i];if(arg.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof arg.value!=="string"){return false}if(hasTraversalPatterns(String(arg.value))){return false}}return true};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&&current.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=[];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{path,pathNode,operation}=extractPathArgument(node,methodName);const method=methodName;if(!isDangerousPath(pathNode,path)){return}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||!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 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectSuspiciousDependencies=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.detectSuspiciousDependencies=(0,eslint_devkit_1.createRule)({name:"detect-suspicious-dependencies",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-suspicious-dependencies.md",description:"Detect typosquatting in package names",cwe:"CWE-506",cvss:7.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Suspicious Dependency",cwe:"CWE-506",description:"Suspicious package name detected - possible typosquatting",severity:"HIGH",fix:"Verify package authenticity on npm registry",documentationLink:"https://cwe.mitre.org/data/definitions/506.html"})},schema:[]},defaultOptions:[],create(context){const popularPackages=["react","lodash","express","axios","webpack"];function levenshtein(a,b){const matrix=[];for(let i=0;i<=b.length;i++){matrix[i]=[i]}for(let j=0;j<=a.length;j++){matrix[0][j]=j}for(let i=1;i<=b.length;i++){for(let j=1;j<=a.length;j++){if(b.charAt(i-1)===a.charAt(j-1)){matrix[i][j]=matrix[i-1][j-1]}else{matrix[i][j]=Math.min(matrix[i-1][j-1]+1,matrix[i][j-1]+1,matrix[i-1][j]+1)}}}return matrix[b.length][a.length]}return{ImportDeclaration(node){const source=node.source.value;if(typeof source==="string"&&!source.startsWith(".")&&!source.startsWith("@")){for(const popular of popularPackages){const distance=levenshtein(source,popular);if(distance>0&&distance<=2){context.report({node,messageId:"violationDetected",data:{name:source,similar:popular}})}}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectSuspiciousDependencies=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.detectSuspiciousDependencies=(0,eslint_devkit_1.createRule)({name:"detect-suspicious-dependencies",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-suspicious-dependencies.md",description:"Detect typosquatting in package names",cwe:"CWE-506",cvss:7.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Suspicious Dependency",cwe:"CWE-506",description:"Suspicious package name detected - possible typosquatting",severity:"HIGH",fix:"Verify package authenticity on npm registry",documentationLink:"https://cwe.mitre.org/data/definitions/506.html"})},schema:[]},defaultOptions:[],create(context){const popularPackages=["react","lodash","express","axios","webpack"];const KNOWN_LEGITIMATE=new Set(["preact","recast","react-dom","reactor","redux","lodash-es","expressive","axios-retry","webpack-cli"]);function levenshtein(a,b){const matrix=[];for(let i=0;i<=b.length;i++){matrix[i]=[i]}for(let j=0;j<=a.length;j++){matrix[0][j]=j}for(let i=1;i<=b.length;i++){for(let j=1;j<=a.length;j++){if(b.charAt(i-1)===a.charAt(j-1)){matrix[i][j]=matrix[i-1][j-1]}else{matrix[i][j]=Math.min(matrix[i-1][j-1]+1,matrix[i][j-1]+1,matrix[i-1][j]+1);if(i>1&&j>1&&b.charAt(i-1)===a.charAt(j-2)&&b.charAt(i-2)===a.charAt(j-1)){matrix[i][j]=Math.min(matrix[i][j],matrix[i-2][j-2]+1)}}}}return matrix[b.length][a.length]}return{ImportDeclaration(node){const source=node.source.value;if(typeof source==="string"&&!source.startsWith(".")&&!source.startsWith("@")){for(const popular of popularPackages){const distance=levenshtein(source,popular);if(distance===1&&!KNOWN_LEGITIMATE.has(source)){context.report({node,messageId:"violationDetected",data:{name:source,similar:popular}})}}}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.lockFile=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.lockFile=(0,eslint_devkit_1.createRule)({name:"lock-file",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/lock-file.md",description:"Ensure package lock file exists for the configured package manager",cwe:"CWE-829",cvss:7.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Lock File Missing",cwe:"CWE-829",description:"Package lock file missing ({{ lockFile }}) for {{ packageManager }}. Commit the lock file to ensure supply chain integrity.",severity:"HIGH",fix:"Generate and commit the {{ lockFile }} file.",documentationLink:"https://cwe.mitre.org/data/definitions/829.html"})},schema:[{type:"object",properties:{packageManager:{type:"string",enum:["npm","yarn","pnpm"],description:"Package manager whose lock file is required"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const fs=require("node:fs");const path=require("node:path");let checked=false;const options=context.options[0]||{};const userPackageManager=options.packageManager;const lockFiles={npm:"package-lock.json",yarn:"yarn.lock",pnpm:"pnpm-lock.yaml"};const targetLockFiles=userPackageManager?[lockFiles[userPackageManager]]:Object.values(lockFiles);const targetLockFile=userPackageManager?lockFiles[userPackageManager]:"package-lock.json | yarn.lock | pnpm-lock.yaml";const reportedManager=userPackageManager??"any";return{Program(node){if(checked)return;checked=true;let dir=path.dirname(context.filename);let found=false;for(let i=0;i<10;i++){for(const lf of targetLockFiles){if(fs.existsSync(path.join(dir,lf))){found=true;break}}if(found)break;const parentDir=path.dirname(dir);if(parentDir===dir)break;dir=parentDir}if(!found){context.report({node,messageId:"violationDetected",data:{packageManager:reportedManager,lockFile:targetLockFile}})}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.lockFile=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const reportedRoots=new Set;exports.lockFile=(0,eslint_devkit_1.createRule)({name:"lock-file",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/lock-file.md",description:"Ensure package lock file exists for the configured package manager",cwe:"CWE-829",cvss:7.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Lock File Missing",cwe:"CWE-829",description:"Package lock file missing ({{ lockFile }}) for {{ packageManager }}. Commit the lock file to ensure supply chain integrity.",severity:"HIGH",fix:"Generate and commit the {{ lockFile }} file.",documentationLink:"https://cwe.mitre.org/data/definitions/829.html"})},schema:[{type:"object",properties:{packageManager:{type:"string",enum:["npm","yarn","pnpm"],description:"Package manager whose lock file is required"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const fs=require("node:fs");const path=require("node:path");const options=context.options[0]||{};const userPackageManager=options.packageManager;const lockFiles={npm:"package-lock.json",yarn:"yarn.lock",pnpm:"pnpm-lock.yaml"};const targetLockFiles=userPackageManager?[lockFiles[userPackageManager]]:Object.values(lockFiles);const targetLockFile=userPackageManager?lockFiles[userPackageManager]:"package-lock.json | yarn.lock | pnpm-lock.yaml";const reportedManager=userPackageManager??"any";return{Program(node){let dir=path.dirname(context.filename);let found=false;for(let i=0;i<10;i++){for(const lf of targetLockFiles){if(fs.existsSync(path.join(dir,lf))){found=true;break}}if(found)break;const parentDir=path.dirname(dir);if(parentDir===dir)break;dir=parentDir}if(!found){let root=path.dirname(context.filename);let manifest=false;for(let i=0;i<10;i++){if(fs.existsSync(path.join(root,"package.json"))){manifest=true;break}const parent=path.dirname(root);if(parent===root)break;root=parent}if(!manifest)return;if(reportedRoots.has(root))return;reportedRoots.add(root);context.report({node,messageId:"violationDetected",data:{packageManager:reportedManager,lockFile:targetLockFile}})}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noBufferOverread=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noBufferOverread=(0,eslint_devkit_1.createRule)({name:"no-buffer-overread",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-buffer-overread.md",description:"Detects buffer access beyond bounds",cwe:"CWE-126"},hasSuggestions:true,messages:{bufferOverread:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer Overread",cwe:"CWE-126",description:"Buffer access beyond allocated bounds",severity:"{{severity}}",fix:"{{safeAlternative}}",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Access",cwe:"CWE-126",description:"Buffer accessed without bounds validation",severity:"HIGH",fix:"Add bounds check before buffer access",documentationLink:"https://nodejs.org/api/buffer.html"}),missingBoundsCheck:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Bounds Check",cwe:"CWE-126",description:"Buffer operation missing bounds validation",severity:"MEDIUM",fix:"Validate indices before buffer operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),negativeBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Negative Buffer Index",cwe:"CWE-126",description:"Negative index used for buffer access",severity:"MEDIUM",fix:"Ensure buffer indices are non-negative",documentationLink:"https://nodejs.org/api/buffer.html"}),userControlledBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"User Controlled Buffer Index",cwe:"CWE-126",description:"Buffer accessed with user-controlled index",severity:"HIGH",fix:"Validate user input before using as buffer index",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferSlice:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Slice",cwe:"CWE-126",description:"Buffer slice with unvalidated indices",severity:"MEDIUM",fix:"Validate slice start/end indices",documentationLink:"https://nodejs.org/api/buffer.html#bufslicestart-end"}),bufferLengthNotChecked:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer Length Not Checked",cwe:"CWE-126",description:"Buffer length not validated before access",severity:"MEDIUM",fix:"Check buffer.length before operations",documentationLink:"https://nodejs.org/api/buffer.html#buflength"}),useSafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safe Buffer Access",description:"Use bounds-checked buffer access methods",severity:"LOW",fix:"Use buffer.read*() with offset validation or safe wrapper functions",documentationLink:"https://nodejs.org/api/buffer.html"}),validateBufferIndices:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Buffer Indices",description:"Validate buffer indices before use",severity:"LOW",fix:"Check 0 <= index < buffer.length",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),checkBufferBounds:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Check Buffer Bounds",description:"Always check buffer bounds",severity:"LOW",fix:"Validate buffer operations against buffer.length",documentationLink:"https://nodejs.org/api/buffer.html#buflength"}),strategyBoundsChecking:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Bounds Checking Strategy",description:"Implement comprehensive bounds checking",severity:"LOW",fix:"Validate all buffer indices and lengths before operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),strategyInputValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Input Validation Strategy",description:"Validate user input used as buffer indices",severity:"LOW",fix:"Sanitize and validate all user input before buffer operations",documentationLink:"https://nodejs.org/api/buffer.html"}),strategySafeBuffers:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Safe Buffer Strategy",description:"Use safe buffer wrapper libraries",severity:"LOW",fix:"Use libraries that provide bounds-checked buffer operations",documentationLink:"https://www.npmjs.com/package/safe-buffer"})},schema:[{type:"object",properties:{bufferMethods:{type:"array",items:{type:"string"},default:["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],description:"Buffer read/write methods checked for bounds"},boundsCheckFunctions:{type:"array",items:{type:"string"},default:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],description:"Function names that count as a bounds check"},bufferTypes:{type:"array",items:{type:"string"},default:["Buffer","Uint8Array","ArrayBuffer","DataView"],description:"Constructor names treated as buffer types"},trustedSanitizers:{type:"array",items:{type:"string"},default:[],description:"Additional function names to consider as buffer index validators"},trustedAnnotations:{type:"array",items:{type:"string"},default:[],description:"Additional JSDoc annotations to consider as safe markers"},strictMode:{type:"boolean",default:false,description:"Disable all false positive detection (strict mode)"}},additionalProperties:false}]},defaultOptions:[{bufferMethods:["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],boundsCheckFunctions:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes:["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const options=context.options[0]||{};const{bufferMethods=["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],boundsCheckFunctions=["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes=["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers=[],trustedAnnotations=[],strictMode=false}=options;const sourceCode=context.sourceCode;const filename=context.filename;const safetyChecker=(0,eslint_devkit_1.createSafetyChecker)({trustedSanitizers,trustedAnnotations,trustedOrmPatterns:[],strictMode});const bufferTypesSet=new Set(bufferTypes.map(t=>t.toLowerCase()));const userControlledKeywords=new Set(["req","request","query","params","input","user","offset","index","body"]);const bufferVars=new Set;const isBufferType=varName=>{if(bufferVars.has(varName))return true;const lowerName=varName.toLowerCase();for(const type of bufferTypesSet){if(lowerName.includes(type))return true}if(lowerName==="buf"||lowerName==="bytes")return true;return false};const isUserControlledIndex=indexNode=>{if(indexNode.type==="MemberExpression"){let walker=indexNode;while(walker.type==="MemberExpression"){walker=walker.object}if(walker.type==="Identifier"){const root=walker.name.toLowerCase();if(["req","request","event","ctx","context"].includes(root)){return true}for(const keyword of userControlledKeywords){if(root.includes(keyword))return true}}}if(indexNode.type==="Identifier"){const varName=indexNode.name.toLowerCase();for(const keyword of userControlledKeywords){if(varName.includes(keyword))return true}let currentScope=sourceCode.getScope(indexNode);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===indexNode.name)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init){const init=def.node.init;if(init.type==="MemberExpression"){const objectText=sourceCode.getText(init.object).toLowerCase();const propertyText=sourceCode.getText(init.property).toLowerCase();const keywords=["req","request","query","params","input","user","body"];if(keywords.some(k=>objectText.includes(k)||propertyText.includes(k))){return true}}if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const typeConversionFunctions=["number","parseint","parsefloat","string","boolean"];let isTypeConversion=false;if(init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){isTypeConversion=typeConversionFunctions.includes(init.callee.name.toLowerCase())}if(isTypeConversion&&init.arguments.length>0){return isUserControlledIndex(init.arguments[0])}}if(init.type==="Identifier"&&init.name!==indexNode.name){return isUserControlledIndex(init)}}}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const typeConversionFunctions=["Number","parseInt","parseFloat","String","Boolean"];if(indexNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&typeConversionFunctions.includes(indexNode.callee.name)){for(const arg of indexNode.arguments){if(isUserControlledIndex(arg)){return true}}}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const text=sourceCode.getText(indexNode).toLowerCase();const keywords=["req.","request.","query.","params.","body.","input.","user."];if(keywords.some(k=>text.includes(k))){return true}}return false};const isIndexValidated=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.value==="number"){return indexNode.value>=0}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&current.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&current.id.name===indexNode.name&&current.init){const init=current.init;if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&boundsCheckFunctions.includes(init.callee.name)){return true}if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&init.callee.object.name==="Math"&&init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(init.callee.property.name==="min"||init.callee.property.name==="max")){return true}break}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){const params=current.params;for(const param of params){if(param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&param.name===indexNode.name){return true}}}current=current.parent}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&indexNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&boundsCheckFunctions.includes(indexNode.callee.name)){return true}return false};const hasBoundsCheck=(bufferName,indexNode)=>{let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){break}if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){const condition=current.test;const conditionText=sourceCode.getText(condition).toLowerCase();if(conditionText.includes(`${bufferName}.length`)&&(conditionText.includes("<")||conditionText.includes("<=")||conditionText.includes(">")||conditionText.includes(">=")||conditionText.includes("&&")||conditionText.includes("||"))){return true}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration){for(const declarator of current.declarations){if(declarator.init){const initText=sourceCode.getText(declarator.init).toLowerCase();if(initText.includes(`${bufferName}.length`)&&(initText.includes("math.min")||initText.includes("math.max")||initText.includes("mathmin")||initText.includes("mathmax"))){return true}}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&current.argument){const returnText=sourceCode.getText(current.argument).toLowerCase();if(returnText.includes(`${bufferName}.length`)){return true}}current=current.parent}return false};const couldBeNegative=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.value==="number"){return indexNode.value<0}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&indexNode.operator==="-"&&indexNode.argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.argument.value==="number"){return true}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&indexNode.operator==="-"){return true}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&current.init){if(current.init.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof current.init.value==="number"&&current.init.value<0){return true}if(current.init.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&current.init.operator==="-"&&current.init.argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof current.init.argument.value==="number"){return true}}current=current.parent}}return false};return{VariableDeclarator(node){if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init){const varName=node.id.name;if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferTypes.includes(node.init.callee.name)){bufferVars.add(varName)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init.callee.object.name==="Buffer"&&node.init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["from","alloc","allocUnsafe"].includes(node.init.callee.property.name)){bufferVars.add(varName)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.init.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)){bufferVars.add(varName)}}if(bufferTypes.some(type=>varName.toLowerCase().includes(type.toLowerCase()))){bufferVars.add(varName)}}},MemberExpression(node){if(node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const bufferName=node.object.name;const indexNode=node.property;if(isBufferType(bufferName)){if(couldBeNegative(indexNode)){context.report({node,messageId:"negativeBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}if(isUserControlledIndex(indexNode)&&!isIndexValidated(indexNode)){if(!hasBoundsCheck(bufferName,indexNode)){if(safetyChecker.isSafe(node,context)){return}context.report({node,messageId:"userControlledBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}}if(!hasBoundsCheck(bufferName,indexNode)&&!isIndexValidated(indexNode)){if(safetyChecker.isSafe(node,context)){return}context.report({node,messageId:"unsafeBufferAccess",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(node.property.type==="Identifier"&&bufferMethods.includes(node.property.name)&&node.object.type==="Identifier"&&isBufferType(node.object.name)){}},CallExpression(node){const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&callee.property.name==="slice"&&callee.object.type==="Identifier"&&isBufferType(callee.object.name)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)){continue}context.report({node:arg,messageId:"unsafeBufferSlice",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object.name)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)){continue}context.report({node:arg,messageId:"missingBoundsCheck",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}},BinaryExpression(node){const leftText=sourceCode.getText(node.left);const rightText=sourceCode.getText(node.right);if(leftText.includes(".length")||rightText.includes(".length")){}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noBufferOverread=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noBufferOverread=(0,eslint_devkit_1.createRule)({name:"no-buffer-overread",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-buffer-overread.md",description:"Detects buffer access beyond bounds",cwe:"CWE-126"},messages:{bufferOverread:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer Overread",cwe:"CWE-126",description:"Buffer access beyond allocated bounds",severity:"{{severity}}",fix:"{{safeAlternative}}",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Access",cwe:"CWE-126",description:"Buffer accessed without bounds validation",severity:"HIGH",fix:"Add bounds check before buffer access",documentationLink:"https://nodejs.org/api/buffer.html"}),missingBoundsCheck:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Bounds Check",cwe:"CWE-126",description:"Buffer operation missing bounds validation",severity:"MEDIUM",fix:"Validate indices before buffer operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),negativeBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Negative Buffer Index",cwe:"CWE-126",description:"Negative index used for buffer access",severity:"MEDIUM",fix:"Ensure buffer indices are non-negative",documentationLink:"https://nodejs.org/api/buffer.html"}),userControlledBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"User Controlled Buffer Index",cwe:"CWE-126",description:"Buffer accessed with user-controlled index",severity:"HIGH",fix:"Validate user input before using as buffer index",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferSlice:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Slice",cwe:"CWE-126",description:"Buffer slice with unvalidated indices",severity:"MEDIUM",fix:"Validate slice start/end indices",documentationLink:"https://nodejs.org/api/buffer.html#bufslicestart-end"}),bufferLengthNotChecked:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer Length Not Checked",cwe:"CWE-126",description:"Buffer length not validated before access",severity:"MEDIUM",fix:"Check buffer.length before operations",documentationLink:"https://nodejs.org/api/buffer.html#buflength"}),useSafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safe Buffer Access",description:"Use bounds-checked buffer access methods",severity:"LOW",fix:"Use buffer.read*() with offset validation or safe wrapper functions",documentationLink:"https://nodejs.org/api/buffer.html"}),validateBufferIndices:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Buffer Indices",description:"Validate buffer indices before use",severity:"LOW",fix:"Check 0 <= index < buffer.length",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),checkBufferBounds:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Check Buffer Bounds",description:"Always check buffer bounds",severity:"LOW",fix:"Validate buffer operations against buffer.length",documentationLink:"https://nodejs.org/api/buffer.html#buflength"}),strategyBoundsChecking:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Bounds Checking Strategy",description:"Implement comprehensive bounds checking",severity:"LOW",fix:"Validate all buffer indices and lengths before operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),strategyInputValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Input Validation Strategy",description:"Validate user input used as buffer indices",severity:"LOW",fix:"Sanitize and validate all user input before buffer operations",documentationLink:"https://nodejs.org/api/buffer.html"}),strategySafeBuffers:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Safe Buffer Strategy",description:"Use safe buffer wrapper libraries",severity:"LOW",fix:"Use libraries that provide bounds-checked buffer operations",documentationLink:"https://www.npmjs.com/package/safe-buffer"})},schema:[{type:"object",properties:{bufferMethods:{type:"array",items:{type:"string"},default:["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],description:"Buffer read/write methods checked for bounds"},boundsCheckFunctions:{type:"array",items:{type:"string"},default:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],description:"Function names that count as a bounds check"},bufferTypes:{type:"array",items:{type:"string"},default:["Buffer","Uint8Array","ArrayBuffer","DataView"],description:"Constructor names treated as buffer types"},trustedSanitizers:{type:"array",items:{type:"string"},default:[],description:"Additional function names to consider as buffer index validators"},trustedAnnotations:{type:"array",items:{type:"string"},default:[],description:"Additional JSDoc annotations to consider as safe markers"},strictMode:{type:"boolean",default:false,description:"Disable all false positive detection (strict mode)"}},additionalProperties:false}]},defaultOptions:[{bufferMethods:["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],boundsCheckFunctions:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes:["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const options=context.options[0]||{};const{bufferMethods=["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],boundsCheckFunctions=["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes=["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers=[],trustedAnnotations=[],strictMode=false}=options;const sourceCode=context.sourceCode;const filename=context.filename;const safetyChecker=(0,eslint_devkit_1.createSafetyChecker)({trustedSanitizers,trustedAnnotations,trustedOrmPatterns:[],strictMode});const bufferTypesSet=new Set(bufferTypes.map(t=>t.toLowerCase()));const userControlledKeywords=new Set(["req","request","query","params","input","user","offset","index","body"]);const bufferVars=new Set;const isBufferType=varName=>{if(bufferVars.has(varName))return true;const lowerName=varName.toLowerCase();for(const type of bufferTypesSet){if(lowerName.includes(type))return true}if(lowerName==="buf"||lowerName==="bytes")return true;return false};const isUserControlledIndex=indexNode=>{if(indexNode.type==="MemberExpression"){let walker=indexNode;while(walker.type==="MemberExpression"){walker=walker.object}if(walker.type==="Identifier"){const root=walker.name.toLowerCase();if(["req","request","event","ctx","context"].includes(root)){return true}for(const keyword of userControlledKeywords){if(root.includes(keyword))return true}}}if(indexNode.type==="Identifier"){const varName=indexNode.name.toLowerCase();for(const keyword of userControlledKeywords){if(varName.includes(keyword))return true}let currentScope=sourceCode.getScope(indexNode);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===indexNode.name)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init){const init=def.node.init;if(init.type==="MemberExpression"){const objectText=sourceCode.getText(init.object).toLowerCase();const propertyText=sourceCode.getText(init.property).toLowerCase();const keywords=["req","request","query","params","input","user","body"];if(keywords.some(k=>objectText.includes(k)||propertyText.includes(k))){return true}}if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const typeConversionFunctions=["number","parseint","parsefloat","string","boolean"];let isTypeConversion=false;if(init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){isTypeConversion=typeConversionFunctions.includes(init.callee.name.toLowerCase())}if(isTypeConversion&&init.arguments.length>0){return isUserControlledIndex(init.arguments[0])}}if(init.type==="Identifier"&&init.name!==indexNode.name){return isUserControlledIndex(init)}}}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const typeConversionFunctions=["Number","parseInt","parseFloat","String","Boolean"];if(indexNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&typeConversionFunctions.includes(indexNode.callee.name)){for(const arg of indexNode.arguments){if(isUserControlledIndex(arg)){return true}}}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const text=sourceCode.getText(indexNode).toLowerCase();const keywords=["req.","request.","query.","params.","body.","input.","user."];if(keywords.some(k=>text.includes(k))){return true}}return false};const isIndexValidated=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.value==="number"){return indexNode.value>=0}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&current.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&current.id.name===indexNode.name&&current.init){const init=current.init;if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&boundsCheckFunctions.includes(init.callee.name)){return true}if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&init.callee.object.name==="Math"&&init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(init.callee.property.name==="min"||init.callee.property.name==="max")){return true}break}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){const params=current.params;for(const param of params){if(param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&param.name===indexNode.name){return true}}}current=current.parent}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&indexNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&boundsCheckFunctions.includes(indexNode.callee.name)){return true}return false};const hasBoundsCheck=(bufferName,indexNode)=>{let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){break}if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){const condition=current.test;const conditionText=sourceCode.getText(condition).toLowerCase();if(conditionText.includes(`${bufferName}.length`)&&(conditionText.includes("<")||conditionText.includes("<=")||conditionText.includes(">")||conditionText.includes(">=")||conditionText.includes("&&")||conditionText.includes("||"))){return true}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration){for(const declarator of current.declarations){if(declarator.init){const initText=sourceCode.getText(declarator.init).toLowerCase();if(initText.includes(`${bufferName}.length`)&&(initText.includes("math.min")||initText.includes("math.max")||initText.includes("mathmin")||initText.includes("mathmax"))){return true}}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&current.argument){const returnText=sourceCode.getText(current.argument).toLowerCase();if(returnText.includes(`${bufferName}.length`)){return true}}current=current.parent}return false};const couldBeNegative=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.value==="number"){return indexNode.value<0}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&indexNode.operator==="-"&&indexNode.argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.argument.value==="number"){return true}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&indexNode.operator==="-"){return true}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&current.init){if(current.init.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof current.init.value==="number"&&current.init.value<0){return true}if(current.init.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&current.init.operator==="-"&&current.init.argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof current.init.argument.value==="number"){return true}}current=current.parent}}return false};return{VariableDeclarator(node){if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init){const varName=node.id.name;if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferTypes.includes(node.init.callee.name)){bufferVars.add(varName)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init.callee.object.name==="Buffer"&&node.init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["from","alloc","allocUnsafe"].includes(node.init.callee.property.name)){bufferVars.add(varName)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.init.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)){bufferVars.add(varName)}}if(bufferTypes.some(type=>varName.toLowerCase().includes(type.toLowerCase()))){bufferVars.add(varName)}}},MemberExpression(node){if(node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const bufferName=node.object.name;const indexNode=node.property;if(isBufferType(bufferName)){if(couldBeNegative(indexNode)){context.report({node,messageId:"negativeBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}if(isUserControlledIndex(indexNode)&&!isIndexValidated(indexNode)){if(!hasBoundsCheck(bufferName,indexNode)){if(safetyChecker.isSafe(node,context)){return}context.report({node,messageId:"userControlledBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}}if(!hasBoundsCheck(bufferName,indexNode)&&!isIndexValidated(indexNode)){if(safetyChecker.isSafe(node,context)){return}context.report({node,messageId:"unsafeBufferAccess",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(node.property.type==="Identifier"&&bufferMethods.includes(node.property.name)&&node.object.type==="Identifier"&&isBufferType(node.object.name)){}},CallExpression(node){const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&callee.property.name==="slice"&&callee.object.type==="Identifier"&&isBufferType(callee.object.name)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)){continue}context.report({node:arg,messageId:"unsafeBufferSlice",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object.name)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)){continue}context.report({node:arg,messageId:"missingBoundsCheck",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}},BinaryExpression(node){const leftText=sourceCode.getText(node.left);const rightText=sourceCode.getText(node.right);if(leftText.includes(".length")||rightText.includes(".length")){}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEFAULT_SECRET_PATTERNS=["token","secret","key","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:7.5},hasSuggestions:true,messages:{timingUnsafeCompare:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Timing-unsafe comparison",cwe:"CWE-208",description:"Using === to compare secrets enables timing attacks. The comparison short-circuits on first mismatch, leaking information about the secret.",severity:"HIGH",fix:"Use crypto.timingSafeEqual() for constant-time comparison",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"}),useTimingSafeEqual:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use timingSafeEqual",description:"Use constant-time comparison to prevent timing attacks",severity:"LOW",fix:"crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS}=options;const patterns=secretPatterns.map(p=>new RegExp(p,"i"));function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return patterns.some(p=>p.test(node.name))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const prop=node.property;if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return patterns.some(p=>p.test(prop.name))}}return false}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}const leftIsSecret=isSecretIdentifier(node.left);const rightIsSecret=isSecretIdentifier(node.right);if(leftIsSecret||rightIsSecret){context.report({node,messageId:"timingUnsafeCompare",suggest:[{messageId:"useTimingSafeEqual",fix:()=>null}]})}}return{BinaryExpression:checkBinaryExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEFAULT_SECRET_PATTERNS=["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];function isExistenceCheck(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.name==="undefined")return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return node.value===null||typeof node.value==="number"||typeof node.value==="boolean"}return false}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:7.5},hasSuggestions:true,messages:{timingUnsafeCompare:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Timing-unsafe comparison",cwe:"CWE-208",description:"Using === to compare secrets enables timing attacks. The comparison short-circuits on first mismatch, leaking information about the secret.",severity:"HIGH",fix:"Use crypto.timingSafeEqual() for constant-time comparison",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"}),useTimingSafeEqual:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use timingSafeEqual",description:"Use constant-time comparison to prevent timing attacks",severity:"LOW",fix:"crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS}=options;const patterns=secretPatterns.map(p=>new RegExp(p,"i"));function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return patterns.some(p=>p.test(node.name))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const prop=node.property;if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return patterns.some(p=>p.test(prop.name))}}return false}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}if(isExistenceCheck(node.left)||isExistenceCheck(node.right)){return}const leftIsSecret=isSecretIdentifier(node.left);const rightIsSecret=isSecretIdentifier(node.right);if(leftIsSecret||rightIsSecret){context.report({node,messageId:"timingUnsafeCompare",suggest:[{messageId:"useTimingSafeEqual",fix:()=>null}]})}}return{BinaryExpression:checkBinaryExpression}}});