eslint-plugin-node-security 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -0
  2. package/package.json +2 -2
  3. package/src/index.js +1 -1
  4. package/src/rules/detect-child-process/index.js +1 -1
  5. package/src/rules/detect-eval-with-expression/index.js +1 -1
  6. package/src/rules/detect-non-literal-fs-filename/index.js +1 -1
  7. package/src/rules/no-arbitrary-file-access/index.js +1 -1
  8. package/src/rules/no-buffer-overread/index.js +1 -1
  9. package/src/rules/no-data-in-temp-storage/index.js +1 -1
  10. package/src/rules/no-deprecated-buffer/index.js +1 -1
  11. package/src/rules/no-deprecated-cipher-method/index.js +1 -1
  12. package/src/rules/no-dynamic-algorithm-selection/index.js +1 -1
  13. package/src/rules/no-dynamic-command-string/index.js +1 -1
  14. package/src/rules/no-dynamic-dependency-loading/index.js +1 -1
  15. package/src/rules/no-ecb-mode/index.js +1 -1
  16. package/src/rules/no-env-injection/index.js +1 -1
  17. package/src/rules/no-insecure-key-derivation/index.js +1 -1
  18. package/src/rules/no-insecure-rsa-padding/index.js +1 -1
  19. package/src/rules/no-math-random-crypto/index.js +1 -1
  20. package/src/rules/no-self-signed-certs/index.js +1 -1
  21. package/src/rules/no-shell-injection/index.js +1 -1
  22. package/src/rules/no-ssrf/index.js +1 -1
  23. package/src/rules/no-timing-unsafe-compare/index.js +1 -1
  24. package/src/rules/no-toctou-vulnerability/index.js +1 -1
  25. package/src/rules/no-unbounded-decompression/index.js +1 -1
  26. package/src/rules/no-unsafe-buffer-alloc/index.js +1 -1
  27. package/src/rules/no-unsafe-dynamic-require/index.js +1 -1
  28. package/src/rules/no-weak-cipher-algorithm/index.js +1 -1
  29. package/src/rules/no-weak-hash-algorithm/index.js +1 -1
  30. package/src/rules/no-zip-slip/index.js +1 -1
  31. package/src/rules/require-aead-tag-verification/index.js +1 -1
  32. package/src/rules/require-secure-credential-storage/index.js +1 -1
  33. package/src/rules/require-secure-deletion/index.js +1 -1
  34. package/src/rules/require-stream-error-handler/index.js +1 -1
  35. package/src/utils/credential-evidence.js +1 -1
  36. package/src/utils/provenance.js +1 -1
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDataInTempStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TEMP_PATHS=["/tmp","/var/tmp","temp/","/temp"];const FS_WRITE_FUNCTIONS=new Set(["writeFile","writeFileSync","appendFile","appendFileSync","createWriteStream","outputFile","outputFileSync"]);const FS_MKDTEMP_FUNCTIONS=new Set(["mkdtemp","mkdtempSync"]);const FS_EQUIVALENTS={"fs-extra":"fs","graceful-fs":"fs"};function containsPathSegments(haystack,needle){const split=value=>value.split(/[/\\]/).filter(segment=>segment.length>0);const target=split(needle);if(target.length===0)return false;const segments=split(haystack);for(let start=0;start+target.length<=segments.length;start+=1){if(target.every((segment,offset)=>segments[start+offset]===segment))return true}return false}function isTmpdirCall(node){return node.type==="CallExpression"&&node.callee.type==="MemberExpression"&&!node.callee.computed&&node.callee.object.type==="Identifier"&&node.callee.object.name==="os"&&node.callee.property.type==="Identifier"&&node.callee.property.name==="tmpdir"}function isStaticTmpdirJoin(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return false;if(callee.property.type!=="Identifier")return false;if(callee.property.name!=="join"&&callee.property.name!=="resolve"){return false}if(callee.object.type!=="Identifier"||callee.object.name!=="path"){return false}if(node.arguments.length<2)return false;if(!isTmpdirCall(node.arguments[0]))return false;return node.arguments.slice(1).every(arg=>(0,eslint_devkit_1.staticString)(arg)!==null)}function isStaticTmpdirTemplate(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return false;let sawTmpdir=false;for(const expression of node.expressions){if(isTmpdirCall(expression)){sawTmpdir=true;continue}if(expression.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof expression.value!=="string"){return false}}return sawTmpdir}function isStaticTmpdirConcat(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.BinaryExpression||node.operator!=="+")return false;const side=part=>{if(isTmpdirCall(part))return"tmpdir";if((0,eslint_devkit_1.staticString)(part)!==null)return"static";if(isStaticTmpdirConcat(part))return"tmpdir";return"other"};const left=side(node.left);const right=side(node.right);if(left==="other"||right==="other")return false;return left==="tmpdir"||right==="tmpdir"}function isPredictableTempPath(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&isStaticTmpdirJoin(node)||isStaticTmpdirTemplate(node)||isStaticTmpdirConcat(node)}exports.noDataInTempStorage=(0,eslint_devkit_1.createRule)({name:"no-data-in-temp-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-data-in-temp-storage.md",description:"Prevent sensitive data in temp directories",cwe:"CWE-312",cvss:5.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Temp Storage Data",cwe:"CWE-312",description:"Sensitive data written to temp directory - not secure",severity:"HIGH",fix:"Use secure storage location or encrypt data before writing",documentationLink:"https://cwe.mitre.org/data/definitions/312.html"}),predictableTempPath:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Predictable Temp File Name (CWE-377)",cwe:"CWE-377",cvss:7.5,description:"path.join(os.tmpdir(), \u2026) with a constant name resolves to the same path on every run, in a directory every local user can write. An attacker who pre-creates that name \u2014 or symlinks it at a file they want clobbered \u2014 wins the race before the write happens.",severity:"HIGH",fix:"Create the file inside a fresh directory from fs.mkdtemp/mkdtempSync, or add a crypto.randomUUID() segment to the name.",documentationLink:"https://cwe.mitre.org/data/definitions/377.html"})},schema:[{type:"object",properties:{tempPaths:{type:"array",items:{type:"string"},default:DEFAULT_TEMP_PATHS,description:"Temporary path prefixes to flag. Replaces the built-in list rather than extending it."},ignoreFiles:{type:"array",items:{type:"string"},default:[],description:"List of files or patterns to ignore"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const options=context.options[0]||{};const tempPaths=options.tempPaths||DEFAULT_TEMP_PATHS;const ignoreFiles=options.ignoreFiles||[];const filename=context.filename;if(ignoreFiles.some(pattern=>filename.includes(pattern))){return{}}function report(node){context.report({node,messageId:"violationDetected"})}function fsEntryPoint(node){const callee=node.callee;const scope=context.sourceCode.getScope(node);const binding=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof callee.property.value==="string"?(base=>base&&{module:base.module,path:[...base.path,callee.property.value]})((0,eslint_devkit_1.resolveModuleBinding)(callee.object,scope,{equivalents:FS_EQUIVALENTS})):(0,eslint_devkit_1.resolveModuleBinding)(callee,scope,{equivalents:FS_EQUIVALENTS});if(binding){const fn=binding.path.at(-1);const prefix=binding.path.slice(0,-1);if(fn===void 0)return void 0;const reachable=binding.module==="fs"&&(prefix.length===0||prefix.length===1&&prefix[0]==="promises")||binding.module==="fs/promises"&&prefix.length===0;return reachable?fn:void 0}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="fs"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return callee.property.name}return void 0}function isFsWriteCall(node){const fn=fsEntryPoint(node);return fn!==void 0&&FS_WRITE_FUNCTIONS.has(fn)}function flowsIntoMkdtemp(identifier){const variable=(0,provenance_1.findVariable)(context.sourceCode,identifier);if(!variable)return false;return variable.references.some(reference=>{const parent=reference.identifier.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;if(parent.arguments[0]!==reference.identifier)return false;const fn=fsEntryPoint(parent);return fn!==void 0&&FS_MKDTEMP_FUNCTIONS.has(fn)})}function reportIfPredictable(candidate,bound){if(!candidate)return;if(!isPredictableTempPath(candidate))return;if(bound!==null&&bound.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&flowsIntoMkdtemp(bound)){return}context.report({node:candidate,messageId:"predictableTempPath"})}function staticPathRuns(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return typeof node.value==="string"?[node.value]:[]}if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.quasis.map(quasi=>quasi.value.raw)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.callee.computed&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.callee.property.name==="join"||node.callee.property.name==="resolve")){return node.arguments.map(arg=>(0,eslint_devkit_1.staticString)(arg)).filter(text=>text!==null)}return[]}function resolvedPathRuns(node){const direct=staticPathRuns(node);if(direct.length>0)return direct;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return[];const variable=(0,provenance_1.findVariable)(context.sourceCode,node);if(!variable)return[];const priorWrites=variable.references.map(reference=>reference.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]);const lastWrite=priorWrites.at(-1);return lastWrite?staticPathRuns(lastWrite):[]}return{CallExpression(node){if(!isFsWriteCall(node))return;const pathArg=node.arguments[0];if(pathArg){const runs=resolvedPathRuns(pathArg);if(runs.some(run=>tempPaths.some(tp=>containsPathSegments(run,tp)))){report(pathArg);return}}reportIfPredictable(pathArg,null)},VariableDeclarator(node){reportIfPredictable(node.init,node.id)},AssignmentExpression(node){reportIfPredictable(node.right,node.left)}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDataInTempStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TEMP_PATHS=["/tmp","/var/tmp","temp/","/temp"];const FS_WRITE_FUNCTIONS=new Set(["writeFile","writeFileSync","appendFile","appendFileSync","createWriteStream","outputFile","outputFileSync"]);const FS_MKDTEMP_FUNCTIONS=new Set(["mkdtemp","mkdtempSync"]);const FS_EQUIVALENTS={"fs-extra":"fs","graceful-fs":"fs"};function containsPathSegments(haystack,needle){const split=value=>value.split(/[/\\]/).filter(segment=>segment.length>0);const target=split(needle);if(target.length===0)return false;const segments=split(haystack);for(let start=0;start+target.length<=segments.length;start+=1){if(target.every((segment,offset)=>segments[start+offset]===segment))return true}return false}function isTmpdirCall(node){return node.type==="CallExpression"&&node.callee.type==="MemberExpression"&&node.callee.object.type==="Identifier"&&node.callee.object.name==="os"&&(0,eslint_devkit_1.propertyName)(node.callee)==="tmpdir"}function isStaticTmpdirJoin(node){const{callee}=node;if(callee.type!=="MemberExpression")return false;const method=(0,eslint_devkit_1.propertyName)(callee);if(method!=="join"&&method!=="resolve"){return false}if(callee.object.type!=="Identifier"||callee.object.name!=="path"){return false}if(node.arguments.length<2)return false;if(!isTmpdirCall(node.arguments[0]))return false;return node.arguments.slice(1).every(arg=>(0,eslint_devkit_1.staticString)(arg)!==null)}function isStaticTmpdirTemplate(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return false;let sawTmpdir=false;for(const expression of node.expressions){if(isTmpdirCall(expression)){sawTmpdir=true;continue}if(expression.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof expression.value!=="string"){return false}}return sawTmpdir}function isStaticTmpdirConcat(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.BinaryExpression||node.operator!=="+")return false;const side=part=>{if(isTmpdirCall(part))return"tmpdir";if((0,eslint_devkit_1.staticString)(part)!==null)return"static";if(isStaticTmpdirConcat(part))return"tmpdir";return"other"};const left=side(node.left);const right=side(node.right);if(left==="other"||right==="other")return false;return left==="tmpdir"||right==="tmpdir"}function isPredictableTempPath(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&isStaticTmpdirJoin(node)||isStaticTmpdirTemplate(node)||isStaticTmpdirConcat(node)}exports.noDataInTempStorage=(0,eslint_devkit_1.createRule)({name:"no-data-in-temp-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-data-in-temp-storage.md",description:"Prevent sensitive data in temp directories",cwe:"CWE-312",cvss:5.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Temp Storage Data",cwe:"CWE-312",description:"Sensitive data written to temp directory - not secure",severity:"HIGH",fix:"Use secure storage location or encrypt data before writing",documentationLink:"https://cwe.mitre.org/data/definitions/312.html"}),predictableTempPath:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Predictable Temp File Name (CWE-377)",cwe:"CWE-377",cvss:7.5,description:"path.join(os.tmpdir(), \u2026) with a constant name resolves to the same path on every run, in a directory every local user can write. An attacker who pre-creates that name \u2014 or symlinks it at a file they want clobbered \u2014 wins the race before the write happens.",severity:"HIGH",fix:"Create the file inside a fresh directory from fs.mkdtemp/mkdtempSync, or add a crypto.randomUUID() segment to the name.",documentationLink:"https://cwe.mitre.org/data/definitions/377.html"})},schema:[{type:"object",properties:{tempPaths:{type:"array",items:{type:"string"},default:DEFAULT_TEMP_PATHS,description:"Temporary path prefixes to flag. Replaces the built-in list rather than extending it."},ignoreFiles:{type:"array",items:{type:"string"},default:[],description:"List of files or patterns to ignore"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const options=context.options[0]||{};const tempPaths=options.tempPaths||DEFAULT_TEMP_PATHS;const ignoreFiles=options.ignoreFiles||[];const filename=context.filename;if(ignoreFiles.some(pattern=>filename.includes(pattern))){return{}}function report(node){context.report({node,messageId:"violationDetected"})}function fsEntryPoint(node){const callee=node.callee;const scope=context.sourceCode.getScope(node);const binding=(0,eslint_devkit_1.resolveModuleBinding)(callee,scope,{equivalents:FS_EQUIVALENTS});if(binding){const fn=binding.path.at(-1);const prefix=binding.path.slice(0,-1);if(fn===void 0)return null;const reachable=binding.module==="fs"&&(prefix.length===0||prefix.length===1&&prefix[0]==="promises")||binding.module==="fs/promises"&&prefix.length===0;return reachable?fn:null}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="fs"&&(0,eslint_devkit_1.propertyName)(callee)!==null){return(0,eslint_devkit_1.propertyName)(callee)}return null}function isFsWriteCall(node){const fn=fsEntryPoint(node);return(0,eslint_devkit_1.namesOneOf)(fn,FS_WRITE_FUNCTIONS)}function flowsIntoMkdtemp(identifier){const variable=(0,provenance_1.findVariable)(context.sourceCode,identifier);if(!variable)return false;return variable.references.some(reference=>{const parent=reference.identifier.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;if(parent.arguments[0]!==reference.identifier)return false;const fn=fsEntryPoint(parent);return(0,eslint_devkit_1.namesOneOf)(fn,FS_MKDTEMP_FUNCTIONS)})}function reportIfPredictable(candidate,bound){if(!candidate)return;if(!isPredictableTempPath(candidate))return;if(bound!==null&&bound.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&flowsIntoMkdtemp(bound)){return}context.report({node:candidate,messageId:"predictableTempPath"})}function staticPathRuns(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return typeof node.value==="string"?[node.value]:[]}if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.quasis.map(quasi=>quasi.value.raw)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&((0,eslint_devkit_1.propertyName)(node.callee)==="join"||(0,eslint_devkit_1.propertyName)(node.callee)==="resolve")){return node.arguments.map(arg=>(0,eslint_devkit_1.staticString)(arg)).filter(text=>text!==null)}return[]}function resolvedPathRuns(node){const direct=staticPathRuns(node);if(direct.length>0)return direct;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return[];const variable=(0,provenance_1.findVariable)(context.sourceCode,node);if(!variable)return[];const priorWrites=variable.references.map(reference=>reference.writeExpr).filter(write=>write!=null).filter(write=>write.range[1]<=node.range[0]).sort((a,b)=>a.range[1]-b.range[1]);const lastWrite=priorWrites.at(-1);return lastWrite?staticPathRuns(lastWrite):[]}return{CallExpression(node){if(!isFsWriteCall(node))return;const pathArg=node.arguments[0];if(pathArg){const runs=resolvedPathRuns(pathArg);if(runs.some(run=>tempPaths.some(tp=>containsPathSegments(run,tp)))){report(pathArg);return}}reportIfPredictable(pathArg,null)},VariableDeclarator(node){reportIfPredictable(node.init,node.id)},AssignmentExpression(node){reportIfPredictable(node.right,node.left)}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDeprecatedBuffer=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const BUFFER_MODULES=new Set(["buffer","node:buffer"]);function isBufferModuleRequire(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[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof node.arguments[0].value==="string"&&BUFFER_MODULES.has(node.arguments[0].value)}function fromBufferModule(def){const declaration=def.parent;return declaration.type===eslint_devkit_1.AST_NODE_TYPES.ImportDeclaration&&BUFFER_MODULES.has(declaration.source.value)}exports.noDeprecatedBuffer=(0,eslint_devkit_1.createRule)({name:"no-deprecated-buffer",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-deprecated-buffer.md",description:"Disallow the deprecated `new Buffer()` constructor and `Buffer()` factory call.",cwe:"CWE-676",cvss:7.5},fixable:"code",messages:{deprecatedBufferConstructor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Deprecated Buffer Constructor",cwe:"CWE-676",cvss:7.5,description:"`new Buffer()` is deprecated since Node 10 and unsafe \u2014 when called with a number it returns uninitialized memory (CVE-2018-7166).",severity:"HIGH",fix:"Use `Buffer.alloc(size)` (zero-filled), `Buffer.allocUnsafe(size)` (only when you immediately overwrite the buffer), or `Buffer.from(value)` (for strings/arrays/buffers).",documentationLink:"https://nodejs.org/api/buffer.html#bufnew-buffersize"}),deprecatedBufferCall:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Deprecated Buffer() Factory Call",cwe:"CWE-676",cvss:7.5,description:"`Buffer()` (without `new`) is deprecated since Node 10 and unsafe \u2014 when called with a number it returns uninitialized memory.",severity:"HIGH",fix:"Use `Buffer.alloc(size)`, `Buffer.allocUnsafe(size)`, or `Buffer.from(value)`.",documentationLink:"https://nodejs.org/api/buffer.html#bufnew-buffersize"})},schema:[]},defaultOptions:[],create(context){const sourceCode=context.sourceCode;function resolvesToNodeBuffer(id){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(variable===null||variable.defs.length===0)return id.name==="Buffer";const def=variable.defs[0];if(def.type==="ImportBinding"){return fromBufferModule(def)&&def.node.type===eslint_devkit_1.AST_NODE_TYPES.ImportSpecifier&&def.node.imported.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&def.node.imported.name==="Buffer"}if(def.type!=="Variable")return false;const init=def.node.init;if(init===null)return false;if(init.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!init.computed&&init.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&init.property.name==="Buffer"){return isBufferModuleRequire(init.object)}if(!isBufferModuleRequire(init))return false;const property=def.name.parent;return property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&!property.computed&&property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.key.name==="Buffer"}function isBufferNamespace(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const variable=(0,provenance_1.findVariable)(sourceCode,node);if(variable===null||variable.defs.length===0){return node.name==="global"||node.name==="globalThis"}const def=variable.defs[0];if(def.type==="ImportBinding"){return fromBufferModule(def)&&def.node.type===eslint_devkit_1.AST_NODE_TYPES.ImportNamespaceSpecifier}return def.type==="Variable"&&def.node.init!==null&&isBufferModuleRequire(def.node.init)}function deprecatedCallee(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return resolvesToNodeBuffer(callee)?callee:null}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.name==="Buffer"&&isBufferNamespace(callee.object)){return callee}return null}function replacementMethod(args){if(args.length!==1)return args.length===0?null:".from";const argument=args[0];if(argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return null;if(argument.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression||argument.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return".from"}const resolved=(0,const_value_1.resolveConstant)(sourceCode,argument);if(resolved===null)return null;return typeof resolved.value==="number"?".alloc":".from"}function fixTo(node,callee,method){return fixer=>{const start=node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression?sourceCode.getFirstToken(node).range[0]:callee.range[0];return fixer.replaceTextRange([start,callee.range[1]],`${sourceCode.getText(callee)}${method}`)}}return{NewExpression(node){const callee=deprecatedCallee(node.callee);if(callee===null)return;const method=replacementMethod(node.arguments);context.report({node,messageId:"deprecatedBufferConstructor",...method===null?{}:{fix:fixTo(node,callee,method)}})},CallExpression(node){const callee=deprecatedCallee(node.callee);if(callee===null)return;const method=replacementMethod(node.arguments);context.report({node,messageId:"deprecatedBufferCall",...method===null?{}:{fix:fixTo(node,callee,method)}})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDeprecatedBuffer=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const BUFFER_MODULES=new Set(["buffer","node:buffer"]);function isBufferModuleRequire(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[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof node.arguments[0].value==="string"&&BUFFER_MODULES.has(node.arguments[0].value)}function fromBufferModule(def){const declaration=def.parent;return declaration.type===eslint_devkit_1.AST_NODE_TYPES.ImportDeclaration&&BUFFER_MODULES.has(declaration.source.value)}exports.noDeprecatedBuffer=(0,eslint_devkit_1.createRule)({name:"no-deprecated-buffer",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-deprecated-buffer.md",description:"Disallow the deprecated `new Buffer()` constructor and `Buffer()` factory call.",cwe:"CWE-676",cvss:7.5},fixable:"code",messages:{deprecatedBufferConstructor:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Deprecated Buffer Constructor",cwe:"CWE-676",cvss:7.5,description:"`new Buffer()` is deprecated since Node 10 and unsafe \u2014 when called with a number it returns uninitialized memory (CVE-2018-7166).",severity:"HIGH",fix:"Use `Buffer.alloc(size)` (zero-filled), `Buffer.allocUnsafe(size)` (only when you immediately overwrite the buffer), or `Buffer.from(value)` (for strings/arrays/buffers).",documentationLink:"https://nodejs.org/api/buffer.html#bufnew-buffersize"}),deprecatedBufferCall:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Deprecated Buffer() Factory Call",cwe:"CWE-676",cvss:7.5,description:"`Buffer()` (without `new`) is deprecated since Node 10 and unsafe \u2014 when called with a number it returns uninitialized memory.",severity:"HIGH",fix:"Use `Buffer.alloc(size)`, `Buffer.allocUnsafe(size)`, or `Buffer.from(value)`.",documentationLink:"https://nodejs.org/api/buffer.html#bufnew-buffersize"})},schema:[]},defaultOptions:[],create(context){const sourceCode=context.sourceCode;function resolvesToNodeBuffer(id){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(variable===null||variable.defs.length===0)return id.name==="Buffer";const def=variable.defs[0];if(def.type==="ImportBinding"){return fromBufferModule(def)&&def.node.type===eslint_devkit_1.AST_NODE_TYPES.ImportSpecifier&&def.node.imported.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&def.node.imported.name==="Buffer"}if(def.type!=="Variable")return false;const init=def.node.init;if(init===null)return false;if(init.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(init)==="Buffer"){return isBufferModuleRequire(init.object)}if(!isBufferModuleRequire(init))return false;const property=def.name.parent;return property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&!property.computed&&property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.key.name==="Buffer"}function isBufferNamespace(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const variable=(0,provenance_1.findVariable)(sourceCode,node);if(variable===null||variable.defs.length===0){return node.name==="global"||node.name==="globalThis"}const def=variable.defs[0];if(def.type==="ImportBinding"){return fromBufferModule(def)&&def.node.type===eslint_devkit_1.AST_NODE_TYPES.ImportNamespaceSpecifier}return def.type==="Variable"&&def.node.init!==null&&isBufferModuleRequire(def.node.init)}function deprecatedCallee(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return resolvesToNodeBuffer(callee)?callee:null}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(callee)==="Buffer"&&isBufferNamespace(callee.object)){return callee}return null}function replacementMethod(args){if(args.length!==1)return args.length===0?null:".from";const argument=args[0];if(argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return null;if(argument.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression||argument.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return".from"}const resolved=(0,const_value_1.resolveConstant)(sourceCode,argument);if(resolved===null)return null;return typeof resolved.value==="number"?".alloc":".from"}function fixTo(node,callee,method){return fixer=>{const start=node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression?sourceCode.getFirstToken(node).range[0]:callee.range[0];return fixer.replaceTextRange([start,callee.range[1]],`${sourceCode.getText(callee)}${method}`)}}return{NewExpression(node){const callee=deprecatedCallee(node.callee);if(callee===null)return;const method=replacementMethod(node.arguments);context.report({node,messageId:"deprecatedBufferConstructor",...method===null?{}:{fix:fixTo(node,callee,method)}})},CallExpression(node){const callee=deprecatedCallee(node.callee);if(callee===null)return;const method=replacementMethod(node.arguments);context.report({node,messageId:"deprecatedBufferCall",...method===null?{}:{fix:fixTo(node,callee,method)}})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDeprecatedCipherMethod=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEPRECATED_METHODS=new Set(["createCipher","createDecipher"]);exports.noDeprecatedCipherMethod=(0,eslint_devkit_1.createRule)({name:"no-deprecated-cipher-method",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-deprecated-cipher-method.md",description:"Disallow deprecated crypto.createCipher/createDecipher methods (use createCipheriv/createDecipheriv instead)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{deprecatedCipherMethod:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Deprecated cipher method",cwe:"CWE-327",description:"crypto.{{method}}() is deprecated. It derives key from password without salt and uses no IV, making encryption deterministic and vulnerable.",severity:"HIGH",fix:"Use crypto.{{replacement}}() with explicit key and random IV",documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"}),useCipheriv:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use createCipheriv",description:"Use createCipheriv with explicit key derivation and random IV",severity:"LOW",fix:"const key = crypto.scryptSync(password, salt, 32);\nconst iv = crypto.randomBytes(16);\nconst cipher = crypto.createCipheriv(algorithm, key, iv);",documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow deprecated methods in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function checkCallExpression(node){if(isTestFile)return;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&DEPRECATED_METHODS.has(node.callee.property.name)){const callee=node.callee;const propertyNode=callee.property;const methodName=propertyNode.name;const replacementName=methodName==="createCipher"?"createCipheriv":"createDecipheriv";context.report({node:propertyNode,messageId:"deprecatedCipherMethod",data:{method:methodName,replacement:replacementName},suggest:[{messageId:"useCipheriv",fix:fixer=>{return fixer.replaceText(propertyNode,replacementName)}}]})}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&DEPRECATED_METHODS.has(node.callee.name)){const methodName=node.callee.name;const replacementName=methodName==="createCipher"?"createCipheriv":"createDecipheriv";context.report({node:node.callee,messageId:"deprecatedCipherMethod",data:{method:methodName,replacement:replacementName},suggest:[{messageId:"useCipheriv",fix:fixer=>{return fixer.replaceText(node.callee,replacementName)}}]})}}return{CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDeprecatedCipherMethod=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEPRECATED_METHODS=new Set(["createCipher","createDecipher"]);exports.noDeprecatedCipherMethod=(0,eslint_devkit_1.createRule)({name:"no-deprecated-cipher-method",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-deprecated-cipher-method.md",description:"Disallow deprecated crypto.createCipher/createDecipher methods (use createCipheriv/createDecipheriv instead)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{deprecatedCipherMethod:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Deprecated cipher method",cwe:"CWE-327",description:"crypto.{{method}}() is deprecated. It derives key from password without salt and uses no IV, making encryption deterministic and vulnerable.",severity:"HIGH",fix:"Use crypto.{{replacement}}() with explicit key and random IV",documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"}),useCipheriv:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use createCipheriv",description:"Use createCipheriv with explicit key derivation and random IV",severity:"LOW",fix:"const key = crypto.scryptSync(password, salt, 32);\nconst iv = crypto.randomBytes(16);\nconst cipher = crypto.createCipheriv(algorithm, key, iv);",documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow deprecated methods in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function checkCallExpression(node){if(isTestFile)return;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node.callee),DEPRECATED_METHODS)){const callee=node.callee;const propertyNode=callee.property;const methodName=propertyNode.name;const replacementName=methodName==="createCipher"?"createCipheriv":"createDecipheriv";context.report({node:propertyNode,messageId:"deprecatedCipherMethod",data:{method:methodName,replacement:replacementName},suggest:[{messageId:"useCipheriv",fix:fixer=>{return fixer.replaceText(propertyNode,replacementName)}}]})}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&DEPRECATED_METHODS.has(node.callee.name)){const methodName=node.callee.name;const replacementName=methodName==="createCipher"?"createCipheriv":"createDecipheriv";context.report({node:node.callee,messageId:"deprecatedCipherMethod",data:{method:methodName,replacement:replacementName},suggest:[{messageId:"useCipheriv",fix:fixer=>{return fixer.replaceText(node.callee,replacementName)}}]})}}return{CallExpression:checkCallExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicAlgorithmSelection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const constant_folding_1=require("../../utils/constant-folding");const CRYPTO_ALGORITHM_FUNCTIONS=new Set(["createHash","createHmac","createSign","createVerify","createCipher","createCipheriv","createDecipher","createDecipheriv"]);exports.noDynamicAlgorithmSelection=(0,eslint_devkit_1.createRule)({name:"no-dynamic-algorithm-selection",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-algorithm-selection.md",description:"Disallow dynamic algorithm names in Node.js crypto functions (CWE-327)",cwe:"CWE-327",cvss:7.5},messages:{dynamicAlgorithm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Dynamic Cryptographic Algorithm (CWE-327)",cwe:"CWE-327",description:"`crypto.{{method}}()` receives a dynamic algorithm name. An attacker who controls this value can downgrade to a weak algorithm (MD5, SHA1, RC4) or cause a crash.",severity:"HIGH",fix:'Hard-code the algorithm name as a literal string (e.g. "sha256", "aes-256-gcm"). Use an allowlist if the algorithm must vary at runtime.',documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html"})},schema:[]},defaultOptions:[],create(context){const sourceCode=context.sourceCode;const isLiteralConstant=(0,constant_folding_1.makeIsLiteralConstant)(sourceCode);const resolvesToLiteral=node=>{if(isLiteralConstant(node))return true;return node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(0,constant_folding_1.parameterIsAlwaysDefault)(sourceCode,node,isLiteralConstant)};return{CallExpression(node){const{callee,arguments:args}=node;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return;if(callee.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return;const objectName=callee.object.name;const methodName=callee.property.name;if(objectName!=="crypto")return;if(!CRYPTO_ALGORITHM_FUNCTIONS.has(methodName))return;const firstArg=args[0];if(!firstArg)return;if(resolvesToLiteral(firstArg))return;context.report({node:firstArg,messageId:"dynamicAlgorithm",data:{method:methodName}})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicAlgorithmSelection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const constant_folding_1=require("../../utils/constant-folding");const CRYPTO_ALGORITHM_FUNCTIONS=new Set(["createHash","createHmac","createSign","createVerify","createCipher","createCipheriv","createDecipher","createDecipheriv"]);exports.noDynamicAlgorithmSelection=(0,eslint_devkit_1.createRule)({name:"no-dynamic-algorithm-selection",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-algorithm-selection.md",description:"Disallow dynamic algorithm names in Node.js crypto functions (CWE-327)",cwe:"CWE-327",cvss:7.5},messages:{dynamicAlgorithm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Dynamic Cryptographic Algorithm (CWE-327)",cwe:"CWE-327",description:"`crypto.{{method}}()` receives a dynamic algorithm name. An attacker who controls this value can downgrade to a weak algorithm (MD5, SHA1, RC4) or cause a crash.",severity:"HIGH",fix:'Hard-code the algorithm name as a literal string (e.g. "sha256", "aes-256-gcm"). Use an allowlist if the algorithm must vary at runtime.',documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html"})},schema:[]},defaultOptions:[],create(context){const sourceCode=context.sourceCode;const isLiteralConstant=(0,constant_folding_1.makeIsLiteralConstant)(sourceCode);const resolvesToLiteral=node=>{if(isLiteralConstant(node))return true;return node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(0,constant_folding_1.parameterIsAlwaysDefault)(sourceCode,node,isLiteralConstant)};return{CallExpression(node){const{callee,arguments:args}=node;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return;if(callee.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return;const methodName=(0,eslint_devkit_1.propertyName)(callee);if(methodName===null)return;const objectName=callee.object.name;if(objectName!=="crypto")return;if(!CRYPTO_ALGORITHM_FUNCTIONS.has(methodName))return;const firstArg=args[0];if(!firstArg)return;if(resolvesToLiteral(firstArg))return;context.report({node:firstArg,messageId:"dynamicAlgorithm",data:{method:methodName}})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicCommandString=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const ARGV_FUNCTIONS=new Set(["spawn","spawnSync","execFile","execFileSync","fork","execa","execaSync"]);const POSIX_COMMAND_FLAGS=new Set(["-c"]);const CMD_COMMAND_FLAGS=new Set(["/c","/C","/k","/K"]);const POWERSHELL_COMMAND_FLAGS=new Set(["-Command","-command","-c","-EncodedCommand","-encodedcommand","-e","-ec"]);const SHELL_COMMAND_FLAGS={sh:POSIX_COMMAND_FLAGS,bash:POSIX_COMMAND_FLAGS,zsh:POSIX_COMMAND_FLAGS,dash:POSIX_COMMAND_FLAGS,ksh:POSIX_COMMAND_FLAGS,busybox:POSIX_COMMAND_FLAGS,cmd:CMD_COMMAND_FLAGS,"cmd.exe":CMD_COMMAND_FLAGS,powershell:POWERSHELL_COMMAND_FLAGS,"powershell.exe":POWERSHELL_COMMAND_FLAGS,pwsh:POWERSHELL_COMMAND_FLAGS};const COMMAND_RUNNERS=new Set(["execaCommand","execaCommandSync","$.raw"]);const POSIX_COMMAND_CLUSTER=/^-[a-zA-Z]*c$/;function isCommandFlag(commandFlags,flag){if(commandFlags.has(flag))return true;return commandFlags===POSIX_COMMAND_FLAGS&&POSIX_COMMAND_CLUSTER.test(flag)}function basename(command){const segments=command.split(/[\\/]/);return segments[segments.length-1]}function shellDialect(command){const shell=basename(command);const flags=SHELL_COMMAND_FLAGS[shell.toLowerCase()];return flags?{shell,flags}:null}function isAssembledString(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.expressions.length>0}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return node.operator==="+"}return node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier||node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression||node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression}function calleeName(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return callee.name;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;if(callee.computed)return null;if(callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="$"){return`$.${callee.property.name}`}return callee.property.name}exports.noDynamicCommandString=(0,eslint_devkit_1.createRule)({name:"no-dynamic-command-string",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-command-string.md",description:"Disallow dynamically assembled command strings passed to a shell flag or to a command-runner that does not escape (CWE-77)",cwe:"CWE-77",cvss:9.8,confidence:"high"},messages:{shellFlagInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Command Injection Through Shell Flag (CWE-77)",cwe:"CWE-77",cvss:9.8,description:'{{fn}}("{{shell}}", ["{{flag}}", \u2026]) hands a dynamically built string to {{shell}}, which parses it as a command line. The argument array looks parameterized but everything after {{flag}} is re-parsed \u2014 `;`, `&&`, backticks and `$()` all execute.',severity:"CRITICAL",fix:'Invoke the target program directly with its own argument array \u2014 spawn("kill", [String(pid)]) \u2014 instead of routing it through a shell.',documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html"}),commandStringInterpolation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Interpolated Command Line (CWE-77)",cwe:"CWE-77",cvss:9.8,description:"{{fn}}() takes a whole command line and does NOT escape interpolated values (unlike the execa/zx tagged-template forms). Any special character in the interpolated value changes which command runs.",severity:"CRITICAL",fix:'Use the tagged-template or array form that escapes for you: execa("git", ["clone", url]) or await $`git clone ${url}`.',documentationLink:"https://cwe.mitre.org/data/definitions/77.html"})},schema:[{type:"object",properties:{extraCommandRunners:{type:"array",items:{type:"string"},default:[],description:"Extra functions that accept a full command line without escaping. Added to the built-in runners, not a replacement for them."}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options]){const{extraCommandRunners}=options;const runners=new Set([...COMMAND_RUNNERS,...extraCommandRunners??[]]);const{sourceCode}=context;function staticStringOf(node){const resolved=(0,const_value_1.resolveConstantString)(sourceCode,node);if(resolved)return resolved.value;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;if(node.computed)return null;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const init=(0,const_value_1.constInitializerOf)(sourceCode,node.object);if(!init)return null;const literal=init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&calleeName(init.callee)==="freeze"&&init.arguments[0]?init.arguments[0]:init;if(literal.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return null;for(const property of literal.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property)continue;if(property.computed)continue;const key=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:property.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof property.key.value==="string"?property.key.value:null;if(key!==node.property.name)continue;return(0,const_value_1.resolveConstantString)(sourceCode,property.value)?.value??null}return null}function isAssembled(node){if(staticStringOf(node)!==null)return false;return isAssembledString(node)}function canonicalName(callee){const direct=calleeName(callee);if(direct!==null&&(ARGV_FUNCTIONS.has(direct)||runners.has(direct))){return direct}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return direct;const init=(0,const_value_1.constInitializerOf)(sourceCode,callee);if(!init)return direct;const target=init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&calleeName(init.callee)==="promisify"?init.arguments[0]:init;if(!target)return direct;const resolved=calleeName(target);if(resolved===null)return direct;return ARGV_FUNCTIONS.has(resolved)||runners.has(resolved)?resolved:direct}function shellPositions(command,argv){const positions=[];const commandText=staticStringOf(command);const outer=commandText===null?null:shellDialect(commandText);if(outer)positions.push({...outer,from:0});argv.elements.forEach((element,index)=>{if(!element)return;const text=staticStringOf(element);if(text===null)return;const nested=shellDialect(text);if(nested)positions.push({...nested,from:index+1})});return positions}function checkShellFlag(node,fn){const argvNode=node.arguments[1];const argv=argvNode?.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?(0,const_value_1.constInitializerOf)(sourceCode,argvNode):argvNode;if(!argv||argv.type!==eslint_devkit_1.AST_NODE_TYPES.ArrayExpression)return;const command=node.arguments[0];for(const{shell,flags,from}of shellPositions(command,argv)){for(let i=from;i<argv.elements.length-1;i+=1){const flag=argv.elements[i];if(!flag)continue;const flagText=staticStringOf(flag);if(flagText===null)continue;if(!isCommandFlag(flags,flagText))continue;const commandString=argv.elements[i+1];if(!commandString)continue;if(!isAssembled(commandString))continue;context.report({node:commandString,messageId:"shellFlagInjection",data:{fn,shell,flag:flagText}});return}}}function checkCommandRunner(node,fn){const commandLine=node.arguments[0];if(!commandLine)return;if(!isAssembled(commandLine))return;context.report({node:commandLine,messageId:"commandStringInterpolation",data:{fn}})}return{CallExpression(node){const fn=canonicalName(node.callee);if(!fn)return;if(ARGV_FUNCTIONS.has(fn)){checkShellFlag(node,fn);return}if(runners.has(fn)){checkCommandRunner(node,fn)}},TaggedTemplateExpression(node){const fn=calleeName(node.tag);if(!fn||!runners.has(fn))return;if(node.quasi.expressions.length===0)return;context.report({node:node.quasi,messageId:"commandStringInterpolation",data:{fn}})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicCommandString=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const ARGV_FUNCTIONS=new Set(["spawn","spawnSync","execFile","execFileSync","fork","execa","execaSync"]);const POSIX_COMMAND_FLAGS=new Set(["-c"]);const CMD_COMMAND_FLAGS=new Set(["/c","/C","/k","/K"]);const POWERSHELL_COMMAND_FLAGS=new Set(["-Command","-command","-c","-EncodedCommand","-encodedcommand","-e","-ec"]);const SHELL_COMMAND_FLAGS={sh:POSIX_COMMAND_FLAGS,bash:POSIX_COMMAND_FLAGS,zsh:POSIX_COMMAND_FLAGS,dash:POSIX_COMMAND_FLAGS,ksh:POSIX_COMMAND_FLAGS,busybox:POSIX_COMMAND_FLAGS,cmd:CMD_COMMAND_FLAGS,"cmd.exe":CMD_COMMAND_FLAGS,powershell:POWERSHELL_COMMAND_FLAGS,"powershell.exe":POWERSHELL_COMMAND_FLAGS,pwsh:POWERSHELL_COMMAND_FLAGS};const COMMAND_RUNNERS=new Set(["execaCommand","execaCommandSync","$.raw"]);const POSIX_COMMAND_CLUSTER=/^-[a-zA-Z]*c$/;function isCommandFlag(commandFlags,flag){if(commandFlags.has(flag))return true;return commandFlags===POSIX_COMMAND_FLAGS&&POSIX_COMMAND_CLUSTER.test(flag)}function basename(command){const segments=command.split(/[\\/]/);return segments[segments.length-1]}function shellDialect(command){const shell=basename(command);const flags=SHELL_COMMAND_FLAGS[shell.toLowerCase()];return flags?{shell,flags}:null}function isAssembledString(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.expressions.length>0}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return node.operator==="+"}return node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier||node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression||node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression}function calleeName(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return callee.name;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;const method=(0,eslint_devkit_1.propertyName)(callee);if(method===null)return null;if(callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="$"){return`$.${method}`}return method}exports.noDynamicCommandString=(0,eslint_devkit_1.createRule)({name:"no-dynamic-command-string",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-command-string.md",description:"Disallow dynamically assembled command strings passed to a shell flag or to a command-runner that does not escape (CWE-77)",cwe:"CWE-77",cvss:9.8,confidence:"high"},messages:{shellFlagInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Command Injection Through Shell Flag (CWE-77)",cwe:"CWE-77",cvss:9.8,description:'{{fn}}("{{shell}}", ["{{flag}}", \u2026]) hands a dynamically built string to {{shell}}, which parses it as a command line. The argument array looks parameterized but everything after {{flag}} is re-parsed \u2014 `;`, `&&`, backticks and `$()` all execute.',severity:"CRITICAL",fix:'Invoke the target program directly with its own argument array \u2014 spawn("kill", [String(pid)]) \u2014 instead of routing it through a shell.',documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html"}),commandStringInterpolation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Interpolated Command Line (CWE-77)",cwe:"CWE-77",cvss:9.8,description:"{{fn}}() takes a whole command line and does NOT escape interpolated values (unlike the execa/zx tagged-template forms). Any special character in the interpolated value changes which command runs.",severity:"CRITICAL",fix:'Use the tagged-template or array form that escapes for you: execa("git", ["clone", url]) or await $`git clone ${url}`.',documentationLink:"https://cwe.mitre.org/data/definitions/77.html"})},schema:[{type:"object",properties:{extraCommandRunners:{type:"array",items:{type:"string"},default:[],description:"Extra functions that accept a full command line without escaping. Added to the built-in runners, not a replacement for them."}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options]){const{extraCommandRunners}=options;const runners=new Set([...COMMAND_RUNNERS,...extraCommandRunners??[]]);const{sourceCode}=context;function staticStringOf(node){const resolved=(0,const_value_1.resolveConstantString)(sourceCode,node);if(resolved)return resolved.value;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;if(node.computed)return null;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const init=(0,const_value_1.constInitializerOf)(sourceCode,node.object);if(!init)return null;const literal=init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&calleeName(init.callee)==="freeze"&&init.arguments[0]?init.arguments[0]:init;if(literal.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return null;for(const property of literal.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property)continue;if(property.computed)continue;const key=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:property.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof property.key.value==="string"?property.key.value:null;if(key!==node.property.name)continue;return(0,const_value_1.resolveConstantString)(sourceCode,property.value)?.value??null}return null}function isAssembled(node){if(staticStringOf(node)!==null)return false;return isAssembledString(node)}function canonicalName(callee){const direct=calleeName(callee);if(direct!==null&&(ARGV_FUNCTIONS.has(direct)||runners.has(direct))){return direct}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return direct;const init=(0,const_value_1.constInitializerOf)(sourceCode,callee);if(!init)return direct;const target=init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&calleeName(init.callee)==="promisify"?init.arguments[0]:init;if(!target)return direct;const resolved=calleeName(target);if(resolved===null)return direct;return ARGV_FUNCTIONS.has(resolved)||runners.has(resolved)?resolved:direct}function shellPositions(command,argv){const positions=[];const commandText=staticStringOf(command);const outer=commandText===null?null:shellDialect(commandText);if(outer)positions.push({...outer,from:0});argv.elements.forEach((element,index)=>{if(!element)return;const text=staticStringOf(element);if(text===null)return;const nested=shellDialect(text);if(nested)positions.push({...nested,from:index+1})});return positions}function checkShellFlag(node,fn){const argvNode=node.arguments[1];const argv=argvNode?.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?(0,const_value_1.constInitializerOf)(sourceCode,argvNode):argvNode;if(!argv||argv.type!==eslint_devkit_1.AST_NODE_TYPES.ArrayExpression)return;const command=node.arguments[0];for(const{shell,flags,from}of shellPositions(command,argv)){for(let i=from;i<argv.elements.length-1;i+=1){const flag=argv.elements[i];if(!flag)continue;const flagText=staticStringOf(flag);if(flagText===null)continue;if(!isCommandFlag(flags,flagText))continue;const commandString=argv.elements[i+1];if(!commandString)continue;if(!isAssembled(commandString))continue;context.report({node:commandString,messageId:"shellFlagInjection",data:{fn,shell,flag:flagText}});return}}}function checkCommandRunner(node,fn){const commandLine=node.arguments[0];if(!commandLine)return;if(!isAssembled(commandLine))return;context.report({node:commandLine,messageId:"commandStringInterpolation",data:{fn}})}return{CallExpression(node){const fn=canonicalName(node.callee);if(!fn)return;if(ARGV_FUNCTIONS.has(fn)){checkShellFlag(node,fn);return}if(runners.has(fn)){checkCommandRunner(node,fn)}},TaggedTemplateExpression(node){const fn=calleeName(node.tag);if(!fn||!runners.has(fn))return;if(node.quasi.expressions.length===0)return;context.report({node:node.quasi,messageId:"commandStringInterpolation",data:{fn}})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicDependencyLoading=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");function isLoaderMember(node){if(node.computed)return false;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.property.name!=="require")return false;const{object}=node;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return object.name==="module";if(object.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||object.computed)return false;if(object.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(object.property.name!=="main")return false;return object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.object.name==="require"}function isLoaderExpression(node){const target=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(target.type===eslint_devkit_1.AST_NODE_TYPES.SequenceExpression){return isLoaderExpression(target.expressions[target.expressions.length-1])}if(target.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return isLoaderMember(target);return target.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&target.name==="require"}exports.noDynamicDependencyLoading=(0,eslint_devkit_1.createRule)({name:"no-dynamic-dependency-loading",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-dependency-loading.md",description:"Prevent runtime dependency injection with dynamic paths",cwe:"CWE-1104",cvss:5.3},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"violation Detected",cwe:"CWE-1104",description:"Dynamic import/require detected - use static imports for security",severity:"HIGH",fix:"Review and apply secure practices",documentationLink:"https://cwe.mitre.org/data/definitions/1104.html"})},schema:[]},defaultOptions:[],create(context){const isSteerable=node=>!(0,eslint_devkit_1.isStaticExpression)({node,scope:context.sourceCode.getScope(node)});const isModuleLoader=callee=>{if(isLoaderExpression(callee))return true;const target=(0,eslint_devkit_1.unwrapTypeSyntax)(callee);if(target.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const init=(0,const_value_1.constInitializerOf)(context.sourceCode,target);if(init===null)return false;if(init.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return isLoaderExpression(init);const binding=(0,eslint_devkit_1.resolveModuleBinding)(init.callee,context.sourceCode.getScope(init));return binding?.module==="module"&&binding.path.join(".")==="createRequire"};return{CallExpression(node){const specifier=node.arguments[0];if(specifier!==void 0&&isModuleLoader(node.callee)&&isSteerable(specifier)){context.report({node,messageId:"violationDetected"})}},ImportExpression(node){if(isSteerable(node.source)){context.report({node,messageId:"violationDetected"})}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicDependencyLoading=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");function isLoaderMember(node){if((0,eslint_devkit_1.propertyName)(node)!=="require")return false;const{object}=node;if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return object.name==="module";if(object.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if((0,eslint_devkit_1.propertyName)(object)!=="main")return false;return object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&object.object.name==="require"}function isLoaderExpression(node){const target=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(target.type===eslint_devkit_1.AST_NODE_TYPES.SequenceExpression){return isLoaderExpression(target.expressions[target.expressions.length-1])}if(target.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return isLoaderMember(target);return target.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&target.name==="require"}exports.noDynamicDependencyLoading=(0,eslint_devkit_1.createRule)({name:"no-dynamic-dependency-loading",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-dependency-loading.md",description:"Prevent runtime dependency injection with dynamic paths",cwe:"CWE-1104",cvss:5.3},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"violation Detected",cwe:"CWE-1104",description:"Dynamic import/require detected - use static imports for security",severity:"HIGH",fix:"Review and apply secure practices",documentationLink:"https://cwe.mitre.org/data/definitions/1104.html"})},schema:[]},defaultOptions:[],create(context){const isSteerable=node=>!(0,eslint_devkit_1.isStaticExpression)({node,scope:context.sourceCode.getScope(node)});const isModuleLoader=callee=>{if(isLoaderExpression(callee))return true;const target=(0,eslint_devkit_1.unwrapTypeSyntax)(callee);if(target.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const init=(0,const_value_1.constInitializerOf)(context.sourceCode,target);if(init===null)return false;if(init.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return isLoaderExpression(init);const binding=(0,eslint_devkit_1.resolveModuleBinding)(init.callee,context.sourceCode.getScope(init));return binding?.module==="module"&&binding.path.join(".")==="createRequire"};return{CallExpression(node){const specifier=node.arguments[0];if(specifier!==void 0&&isModuleLoader(node.callee)&&isSteerable(specifier)){context.report({node,messageId:"violationDetected"})}},ImportExpression(node){if(isSteerable(node.source)){context.report({node,messageId:"violationDetected"})}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noEcbMode=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");exports.noEcbMode=(0,eslint_devkit_1.createRule)({name:"no-ecb-mode",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-ecb-mode.md",description:"Disallow ECB encryption mode (use GCM or CBC instead)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{ecbMode:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"ECB mode detected",cwe:"CWE-327",description:'ECB mode encrypts identical plaintext blocks to identical ciphertext, leaking data patterns. Famous example: the "ECB penguin".',severity:"HIGH",fix:'Use GCM mode for authenticated encryption: crypto.createCipheriv("aes-256-gcm", key, iv)',documentationLink:"https://blog.cloudflare.com/why-are-some-images-more-secure-than-others/"}),useGcm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use GCM mode",description:"GCM provides authenticated encryption (confidentiality + integrity)",severity:"LOW",fix:'crypto.createCipheriv("aes-256-gcm", key, iv)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"}),useCbc:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use CBC mode",description:"CBC with HMAC provides confidentiality (add separate MAC for integrity)",severity:"LOW",fix:'crypto.createCipheriv("aes-256-cbc", key, iv)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow ECB mode in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function checkCallExpression(node){if(isTestFile)return;const cipherMethods=new Set(["createCipher","createCipheriv","createDecipher","createDecipheriv"]);const isCipherCall=node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&cipherMethods.has(node.callee.property.name)||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&cipherMethods.has(node.callee.name);if(isCipherCall&&node.arguments.length>=1){const algorithmArg=node.arguments[0];const resolved=(0,const_value_1.resolveConstantString)(context.sourceCode,algorithmArg);if(resolved===null)return;const algorithm=resolved.value.toLowerCase();if(algorithm.includes("-ecb")||algorithm.endsWith("ecb")){const gcmReplacement=algorithm.replace(/-?ecb$/,"-gcm");const target=resolved.source;context.report({node:algorithmArg,messageId:"ecbMode",suggest:[{messageId:"useGcm",fix:fixer=>{return fixer.replaceText(target,`"${gcmReplacement}"`)}},{messageId:"useCbc",fix:fixer=>{const cbcReplacement=algorithm.replace(/-?ecb$/,"-cbc");return fixer.replaceText(target,`"${cbcReplacement}"`)}}]})}}}return{CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noEcbMode=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");exports.noEcbMode=(0,eslint_devkit_1.createRule)({name:"no-ecb-mode",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-ecb-mode.md",description:"Disallow ECB encryption mode (use GCM or CBC instead)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{ecbMode:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"ECB mode detected",cwe:"CWE-327",description:'ECB mode encrypts identical plaintext blocks to identical ciphertext, leaking data patterns. Famous example: the "ECB penguin".',severity:"HIGH",fix:'Use GCM mode for authenticated encryption: crypto.createCipheriv("aes-256-gcm", key, iv)',documentationLink:"https://blog.cloudflare.com/why-are-some-images-more-secure-than-others/"}),useGcm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use GCM mode",description:"GCM provides authenticated encryption (confidentiality + integrity)",severity:"LOW",fix:'crypto.createCipheriv("aes-256-gcm", key, iv)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"}),useCbc:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use CBC mode",description:"CBC with HMAC provides confidentiality (add separate MAC for integrity)",severity:"LOW",fix:'crypto.createCipheriv("aes-256-cbc", key, iv)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow ECB mode in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function checkCallExpression(node){if(isTestFile)return;const cipherMethods=new Set(["createCipher","createCipheriv","createDecipher","createDecipheriv"]);const isCipherCall=node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node.callee),cipherMethods)||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&cipherMethods.has(node.callee.name);if(isCipherCall&&node.arguments.length>=1){const algorithmArg=node.arguments[0];const resolved=(0,const_value_1.resolveConstantString)(context.sourceCode,algorithmArg);if(resolved===null)return;const algorithm=resolved.value.toLowerCase();if(algorithm.includes("-ecb")||algorithm.endsWith("ecb")){const gcmReplacement=algorithm.replace(/-?ecb$/,"-gcm");const target=resolved.source;context.report({node:algorithmArg,messageId:"ecbMode",suggest:[{messageId:"useGcm",fix:fixer=>{return fixer.replaceText(target,`"${gcmReplacement}"`)}},{messageId:"useCbc",fix:fixer=>{const cbcReplacement=algorithm.replace(/-?ecb$/,"-cbc");return fixer.replaceText(target,`"${cbcReplacement}"`)}}]})}}}return{CallExpression:checkCallExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noEnvInjection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEFAULT_REQUEST_ROOTS=["req","request","ctx","event"];const MAX_TRACE_DEPTH=3;function isProcessEnv(node){return node.type==="MemberExpression"&&!node.computed&&node.object.type==="Identifier"&&node.object.name==="process"&&node.property.type==="Identifier"&&node.property.name==="env"}function memberChainRoot(node){let current=node;while(current.type==="MemberExpression"){current=current.object}return current.type==="Identifier"?current:null}exports.noEnvInjection=(0,eslint_devkit_1.createRule)({name:"no-env-injection",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-env-injection.md",description:"Disallow writing process.env under a key the caller controls, which can overwrite PATH, NODE_OPTIONS or LD_PRELOAD (CWE-99)",cwe:"CWE-99",cvss:8.8,confidence:"high"},messages:{envKeyInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Environment Variable Injection (CWE-99)",cwe:"CWE-99",cvss:8.8,description:'process.env[\u2026] is written under a key that traces back to the request, so the caller decides WHICH variable is set. PATH changes which binary every later spawn/exec resolves; NODE_OPTIONS="--require ./x.js" and LD_PRELOAD run attacker code in every child process. The value being validated does not help \u2014 the name is the vulnerability.',severity:"HIGH",fix:'Map the caller\'s input through a fixed allowlist before it reaches the key: `const ALLOWED = { locale: "APP_LOCALE" }; const name = ALLOWED[input]; if (!name) return; process.env[name] = value;`. Better still, keep request-scoped settings out of the process environment entirely.',documentationLink:"https://cwe.mitre.org/data/definitions/99.html"}),envBulkInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Bulk Environment Overwrite (CWE-99)",cwe:"CWE-99",cvss:8.8,description:"Object.assign(process.env, \u2026) copies every key of a request-derived object into the environment, so the caller chooses the names AND the values wholesale \u2014 PATH, NODE_OPTIONS and LD_PRELOAD included.",severity:"HIGH",fix:"Copy only the keys you name yourself, from a fixed allowlist, instead of spreading the request object into the environment.",documentationLink:"https://cwe.mitre.org/data/definitions/99.html"})},schema:[{type:"object",properties:{requestRootNames:{type:"array",items:{type:"string"},default:[...DEFAULT_REQUEST_ROOTS],description:"Identifiers that name the request at the top of a handler. Replaces the default."},extraRequestRoots:{type:"array",items:{type:"string"},default:[],description:"Extra identifiers to treat as roots of request-controlled data. Added to the built-in roots, not a replacement for them."}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options]){const{extraRequestRoots,requestRootNames}=options;const requestRoots=new Set([...requestRootNames??DEFAULT_REQUEST_ROOTS,...extraRequestRoots??[]]);function findVariable(node){let scope=context.sourceCode.getScope(node);while(scope){const found=scope.variables.find(v=>v.name===node.name);if(found)return found;scope=scope.upper}return null}function isRequestDerived(node,depth){if(depth>MAX_TRACE_DEPTH)return false;if(node.type==="MemberExpression"){const root=memberChainRoot(node);return root!==null&&requestRoots.has(root.name)}if(node.type!=="Identifier")return false;if(requestRoots.has(node.name))return true;const variable=findVariable(node);if(!variable)return false;if(variable.references.filter(ref=>ref.isWrite()).length!==1){return false}const[def]=variable.defs;if(!def||def.node.type!=="VariableDeclarator")return false;const init=def.node.init;return init!=null&&isRequestDerived(init,depth+1)}return{AssignmentExpression(node){const{left}=node;if(left.type!=="MemberExpression"||!left.computed)return;if(!isProcessEnv(left.object))return;if(!isRequestDerived(left.property,0))return;context.report({node:left.property,messageId:"envKeyInjection"})},CallExpression(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return;if(callee.property.type!=="Identifier")return;if(callee.property.name!=="assign")return;if(callee.object.type!=="Identifier")return;if(callee.object.name!=="Object")return;const[target,...sources]=node.arguments;if(!target||!isProcessEnv(target))return;for(const source of sources){if(isRequestDerived(source,0)){context.report({node:source,messageId:"envBulkInjection"});return}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noEnvInjection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEFAULT_REQUEST_ROOTS=["req","request","ctx","event"];const MAX_TRACE_DEPTH=3;function isProcessEnv(node){return node.type==="MemberExpression"&&node.object.type==="Identifier"&&node.object.name==="process"&&(0,eslint_devkit_1.propertyName)(node)==="env"}function memberChainRoot(node){let current=node;while(current.type==="MemberExpression"){current=current.object}return current.type==="Identifier"?current:null}exports.noEnvInjection=(0,eslint_devkit_1.createRule)({name:"no-env-injection",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-env-injection.md",description:"Disallow writing process.env under a key the caller controls, which can overwrite PATH, NODE_OPTIONS or LD_PRELOAD (CWE-99)",cwe:"CWE-99",cvss:8.8,confidence:"high"},messages:{envKeyInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Environment Variable Injection (CWE-99)",cwe:"CWE-99",cvss:8.8,description:'process.env[\u2026] is written under a key that traces back to the request, so the caller decides WHICH variable is set. PATH changes which binary every later spawn/exec resolves; NODE_OPTIONS="--require ./x.js" and LD_PRELOAD run attacker code in every child process. The value being validated does not help \u2014 the name is the vulnerability.',severity:"HIGH",fix:'Map the caller\'s input through a fixed allowlist before it reaches the key: `const ALLOWED = { locale: "APP_LOCALE" }; const name = ALLOWED[input]; if (!name) return; process.env[name] = value;`. Better still, keep request-scoped settings out of the process environment entirely.',documentationLink:"https://cwe.mitre.org/data/definitions/99.html"}),envBulkInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Bulk Environment Overwrite (CWE-99)",cwe:"CWE-99",cvss:8.8,description:"Object.assign(process.env, \u2026) copies every key of a request-derived object into the environment, so the caller chooses the names AND the values wholesale \u2014 PATH, NODE_OPTIONS and LD_PRELOAD included.",severity:"HIGH",fix:"Copy only the keys you name yourself, from a fixed allowlist, instead of spreading the request object into the environment.",documentationLink:"https://cwe.mitre.org/data/definitions/99.html"})},schema:[{type:"object",properties:{requestRootNames:{type:"array",items:{type:"string"},default:[...DEFAULT_REQUEST_ROOTS],description:"Identifiers that name the request at the top of a handler. Replaces the default."},extraRequestRoots:{type:"array",items:{type:"string"},default:[],description:"Extra identifiers to treat as roots of request-controlled data. Added to the built-in roots, not a replacement for them."}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options]){const{extraRequestRoots,requestRootNames}=options;const requestRoots=new Set([...requestRootNames??DEFAULT_REQUEST_ROOTS,...extraRequestRoots??[]]);function findVariable(node){let scope=context.sourceCode.getScope(node);while(scope){const found=scope.variables.find(v=>v.name===node.name);if(found)return found;scope=scope.upper}return null}function isRequestDerived(node,depth){if(depth>MAX_TRACE_DEPTH)return false;if(node.type==="MemberExpression"){const root=memberChainRoot(node);return root!==null&&requestRoots.has(root.name)}if(node.type!=="Identifier")return false;if(requestRoots.has(node.name))return true;const variable=findVariable(node);if(!variable)return false;if(variable.references.filter(ref=>ref.isWrite()).length!==1){return false}const[def]=variable.defs;if(!def||def.node.type!=="VariableDeclarator")return false;const init=def.node.init;return init!=null&&isRequestDerived(init,depth+1)}return{AssignmentExpression(node){const{left}=node;if(left.type!=="MemberExpression"||!left.computed)return;if(!isProcessEnv(left.object))return;if(!isRequestDerived(left.property,0))return;context.report({node:left.property,messageId:"envKeyInjection"})},CallExpression(node){const{callee}=node;if(callee.type!=="MemberExpression")return;if((0,eslint_devkit_1.propertyName)(callee)!=="assign")return;if(callee.object.type!=="Identifier")return;if(callee.object.name!=="Object")return;const[target,...sources]=node.arguments;if(!target||!isProcessEnv(target))return;for(const source of sources){if(isRequestDerived(source,0)){context.report({node:source,messageId:"envBulkInjection"});return}}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noInsecureKeyDerivation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const PBKDF2_EXPORTS=new Set(["pbkdf2","pbkdf2Sync"]);const SUBTLE_DERIVE_METHODS=new Set(["deriveBits","deriveKey"]);function foldNumber(sourceCode,node,depth=0){if(depth>6)return null;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return foldNumber(sourceCode,bare,depth+1);if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return typeof node.value==="number"?{value:node.value,source:node}:null}if(node.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&node.operator==="-"){const inner=foldNumber(sourceCode,node.argument,depth+1);return inner===null?null:{value:-inner.value,source:node}}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){const left=foldNumber(sourceCode,node.left,depth+1);const right=foldNumber(sourceCode,node.right,depth+1);if(left===null||right===null)return null;switch(node.operator){case"*":return{value:left.value*right.value,source:node};case"+":return{value:left.value+right.value,source:node};case"-":return{value:left.value-right.value,source:node};case"**":return{value:left.value**right.value,source:node};default:return null}}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,const_value_1.constInitializerOf)(sourceCode,node);return init===null?null:foldNumber(sourceCode,init,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const value=objectPropertyValue(sourceCode,node.object,propertyKey(sourceCode,node));return value===null?null:foldNumber(sourceCode,value,depth+1)}return null}function propertyKey(sourceCode,node){if(node.computed)return(0,const_value_1.resolveConstantString)(sourceCode,node.property)?.value??null;return node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node.property.name:null}function objectPropertyValue(sourceCode,node,key){if(key===null)return null;let object=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,const_value_1.constInitializerOf)(sourceCode,object);if(init===null)return null;object=(0,eslint_devkit_1.unwrapTypeSyntax)(init)}if(object.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return null;for(const property of object.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.computed)continue;const name=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:String(property.key.value);if(name===key)return property.value}return null}function isPromisifyCall(sourceCode,node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,sourceCode.getScope(node));return binding?.module==="util"&&binding.path.at(-1)==="promisify"}function isPbkdf2Callee(sourceCode,callee,depth=0){if(depth>4)return false;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&PBKDF2_EXPORTS.has(callee.property.name)){return true}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&PBKDF2_EXPORTS.has(callee.name))return true;const binding=(0,eslint_devkit_1.resolveModuleBinding)(callee,sourceCode.getScope(callee));if(binding!==void 0&&PBKDF2_EXPORTS.has(binding.path.at(-1)??""))return true;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){return isPromisifyCall(sourceCode,callee)&&callee.arguments.length>0&&isPbkdf2Callee(sourceCode,callee.arguments[0],depth+1)}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,const_value_1.constInitializerOf)(sourceCode,callee);return init!==null&&isPromisifyCall(sourceCode,init)&&init.arguments.length>0&&isPbkdf2Callee(sourceCode,init.arguments[0],depth+1)}return false}const DEFAULT_MIN_ITERATIONS=1e5;exports.noInsecureKeyDerivation=(0,eslint_devkit_1.createRule)({name:"no-insecure-key-derivation",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-insecure-key-derivation.md",description:"Disallow PBKDF2 with insufficient iterations (< 100,000)",cwe:"CWE-916",cvss:7.5},hasSuggestions:true,messages:{insufficientIterations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Insufficient PBKDF2 iterations",cwe:"CWE-916",description:"PBKDF2 with {{actual}} iterations is too low. Minimum recommended: {{minimum}} iterations (OWASP 2023).",severity:"HIGH",fix:"Increase iterations to at least {{minimum}}, or use scrypt/Argon2",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html"}),useMinIterations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use minimum iterations",description:"Use at least {{minimum}} iterations for PBKDF2",severity:"LOW",fix:"crypto.pbkdf2(password, salt, {{minimum}}, keylen, digest)",documentationLink:"https://nodejs.org/api/crypto.html#cryptopbkdf2password-salt-iterations-keylen-digest-callback"})},schema:[{type:"object",properties:{minIterations:{type:"number",default:DEFAULT_MIN_ITERATIONS,description:"Minimum required PBKDF2 iterations"}},additionalProperties:false}]},defaultOptions:[{minIterations:DEFAULT_MIN_ITERATIONS}],create(context,[options={}]){const{minIterations=DEFAULT_MIN_ITERATIONS}=options;const sourceCode=context.sourceCode;function judgeIterations(iterationsArg){if(iterationsArg===void 0||iterationsArg===null)return;const folded=foldNumber(sourceCode,iterationsArg);if(folded===null||folded.value>=minIterations)return;context.report({node:iterationsArg,messageId:"insufficientIterations",data:{actual:String(folded.value),minimum:String(minIterations)},suggest:[{messageId:"useMinIterations",data:{minimum:String(minIterations)},fix:fixer=>{return fixer.replaceText(folded.source,String(minIterations))}}]})}function checkCallExpression(node){if(isPbkdf2Callee(sourceCode,node.callee)){judgeIterations(node.arguments[2]);return}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.name==="PBKDF2"||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="PBKDF2"){const options2=node.arguments[2];if(options2!==void 0){judgeIterations(objectPropertyValue(sourceCode,options2,"iterations"))}return}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&SUBTLE_DERIVE_METHODS.has(node.callee.property.name)){const params=node.arguments[0];if(params===void 0)return;const algorithm=objectPropertyValue(sourceCode,params,"name");if(algorithm===null)return;if((0,const_value_1.resolveConstantString)(sourceCode,algorithm)?.value!=="PBKDF2")return;judgeIterations(objectPropertyValue(sourceCode,params,"iterations"))}}return{CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noInsecureKeyDerivation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const PBKDF2_EXPORTS=new Set(["pbkdf2","pbkdf2Sync"]);const SUBTLE_DERIVE_METHODS=new Set(["deriveBits","deriveKey"]);function foldNumber(sourceCode,node,depth=0){if(depth>6)return null;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return foldNumber(sourceCode,bare,depth+1);if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal){return typeof node.value==="number"?{value:node.value,source:node}:null}if(node.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&node.operator==="-"){const inner=foldNumber(sourceCode,node.argument,depth+1);return inner===null?null:{value:-inner.value,source:node}}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){const left=foldNumber(sourceCode,node.left,depth+1);const right=foldNumber(sourceCode,node.right,depth+1);if(left===null||right===null)return null;switch(node.operator){case"*":return{value:left.value*right.value,source:node};case"+":return{value:left.value+right.value,source:node};case"-":return{value:left.value-right.value,source:node};case"**":return{value:left.value**right.value,source:node};default:return null}}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,const_value_1.constInitializerOf)(sourceCode,node);return init===null?null:foldNumber(sourceCode,init,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const value=objectPropertyValue(sourceCode,node.object,propertyKey(sourceCode,node));return value===null?null:foldNumber(sourceCode,value,depth+1)}return null}function propertyKey(sourceCode,node){if(node.computed)return(0,const_value_1.resolveConstantString)(sourceCode,node.property)?.value??null;return node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?node.property.name:null}function objectPropertyValue(sourceCode,node,key){if(key===null)return null;let object=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,const_value_1.constInitializerOf)(sourceCode,object);if(init===null)return null;object=(0,eslint_devkit_1.unwrapTypeSyntax)(init)}if(object.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return null;for(const property of object.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.computed)continue;const name=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:String(property.key.value);if(name===key)return property.value}return null}function isPromisifyCall(sourceCode,node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,sourceCode.getScope(node));return binding?.module==="util"&&binding.path.at(-1)==="promisify"}function isPbkdf2Callee(sourceCode,callee,depth=0){if(depth>4)return false;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),PBKDF2_EXPORTS)){return true}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&PBKDF2_EXPORTS.has(callee.name))return true;const binding=(0,eslint_devkit_1.resolveModuleBinding)(callee,sourceCode.getScope(callee));if(binding!==void 0&&PBKDF2_EXPORTS.has(binding.path.at(-1)??""))return true;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){return isPromisifyCall(sourceCode,callee)&&callee.arguments.length>0&&isPbkdf2Callee(sourceCode,callee.arguments[0],depth+1)}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,const_value_1.constInitializerOf)(sourceCode,callee);return init!==null&&isPromisifyCall(sourceCode,init)&&init.arguments.length>0&&isPbkdf2Callee(sourceCode,init.arguments[0],depth+1)}return false}const DEFAULT_MIN_ITERATIONS=1e5;exports.noInsecureKeyDerivation=(0,eslint_devkit_1.createRule)({name:"no-insecure-key-derivation",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-insecure-key-derivation.md",description:"Disallow PBKDF2 with insufficient iterations (< 100,000)",cwe:"CWE-916",cvss:7.5},hasSuggestions:true,messages:{insufficientIterations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Insufficient PBKDF2 iterations",cwe:"CWE-916",description:"PBKDF2 with {{actual}} iterations is too low. Minimum recommended: {{minimum}} iterations (OWASP 2023).",severity:"HIGH",fix:"Increase iterations to at least {{minimum}}, or use scrypt/Argon2",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html"}),useMinIterations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use minimum iterations",description:"Use at least {{minimum}} iterations for PBKDF2",severity:"LOW",fix:"crypto.pbkdf2(password, salt, {{minimum}}, keylen, digest)",documentationLink:"https://nodejs.org/api/crypto.html#cryptopbkdf2password-salt-iterations-keylen-digest-callback"})},schema:[{type:"object",properties:{minIterations:{type:"number",default:DEFAULT_MIN_ITERATIONS,description:"Minimum required PBKDF2 iterations"}},additionalProperties:false}]},defaultOptions:[{minIterations:DEFAULT_MIN_ITERATIONS}],create(context,[options={}]){const{minIterations=DEFAULT_MIN_ITERATIONS}=options;const sourceCode=context.sourceCode;function judgeIterations(iterationsArg){if(iterationsArg===void 0||iterationsArg===null)return;const folded=foldNumber(sourceCode,iterationsArg);if(folded===null||folded.value>=minIterations)return;context.report({node:iterationsArg,messageId:"insufficientIterations",data:{actual:String(folded.value),minimum:String(minIterations)},suggest:[{messageId:"useMinIterations",data:{minimum:String(minIterations)},fix:fixer=>{return fixer.replaceText(folded.source,String(minIterations))}}]})}function checkCallExpression(node){if(isPbkdf2Callee(sourceCode,node.callee)){judgeIterations(node.arguments[2]);return}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(node.callee)==="PBKDF2"||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="PBKDF2"){const options2=node.arguments[2];if(options2!==void 0){judgeIterations(objectPropertyValue(sourceCode,options2,"iterations"))}return}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node.callee),SUBTLE_DERIVE_METHODS)){const params=node.arguments[0];if(params===void 0)return;const algorithm=objectPropertyValue(sourceCode,params,"name");if(algorithm===null)return;if((0,const_value_1.resolveConstantString)(sourceCode,algorithm)?.value!=="PBKDF2")return;judgeIterations(objectPropertyValue(sourceCode,params,"iterations"))}}return{CallExpression:checkCallExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noInsecureRsaPadding=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const PKCS1_PADDING_NAMES=new Set(["RSA_PKCS1_PADDING","constants.RSA_PKCS1_PADDING","crypto.constants.RSA_PKCS1_PADDING"]);exports.noInsecureRsaPadding=(0,eslint_devkit_1.createRule)({name:"no-insecure-rsa-padding",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-insecure-rsa-padding.md",description:"Disallow RSA PKCS#1 v1.5 padding (CVE-2023-46809 Marvin Attack)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{insecureRsaPadding:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Insecure RSA padding",cwe:"CWE-327",description:"RSA PKCS#1 v1.5 padding is vulnerable to the Marvin Attack (CVE-2023-46809). Timing side-channels allow attackers to decrypt ciphertexts or forge signatures.",severity:"HIGH",fix:"Use RSA_PKCS1_OAEP_PADDING instead",documentationLink:"https://people.redhat.com/~hkario/marvin/"}),useOaep:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use OAEP padding",description:"Use RSA-OAEP which is not vulnerable to padding oracle attacks",severity:"LOW",fix:"padding: crypto.constants.RSA_PKCS1_OAEP_PADDING",documentationLink:"https://nodejs.org/api/crypto.html#cryptopublicdecryptkey-buffer"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);const sourceCode=context.sourceCode;function checkCallExpression(node){if(isTestFile)return;const rsaMethods=new Set(["privateDecrypt","publicDecrypt","privateEncrypt","publicEncrypt"]);const isRsaCall=node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&rsaMethods.has(node.callee.property.name)||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&rsaMethods.has(node.callee.name);if(isRsaCall&&node.arguments.length>=1){const keyArg=node.arguments[0];if(keyArg.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){for(const prop of keyArg.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&prop.key.name==="padding"){const paddingText=sourceCode.getText(prop.value);if(PKCS1_PADDING_NAMES.has(paddingText)||paddingText.includes("RSA_PKCS1_PADDING")){context.report({node:prop,messageId:"insecureRsaPadding",suggest:[{messageId:"useOaep",fix:fixer=>{return fixer.replaceText(prop.value,"crypto.constants.RSA_PKCS1_OAEP_PADDING")}}]})}}}}}}return{CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noInsecureRsaPadding=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const PKCS1_PADDING_NAMES=new Set(["RSA_PKCS1_PADDING","constants.RSA_PKCS1_PADDING","crypto.constants.RSA_PKCS1_PADDING"]);exports.noInsecureRsaPadding=(0,eslint_devkit_1.createRule)({name:"no-insecure-rsa-padding",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-insecure-rsa-padding.md",description:"Disallow RSA PKCS#1 v1.5 padding (CVE-2023-46809 Marvin Attack)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{insecureRsaPadding:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Insecure RSA padding",cwe:"CWE-327",description:"RSA PKCS#1 v1.5 padding is vulnerable to the Marvin Attack (CVE-2023-46809). Timing side-channels allow attackers to decrypt ciphertexts or forge signatures.",severity:"HIGH",fix:"Use RSA_PKCS1_OAEP_PADDING instead",documentationLink:"https://people.redhat.com/~hkario/marvin/"}),useOaep:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use OAEP padding",description:"Use RSA-OAEP which is not vulnerable to padding oracle attacks",severity:"LOW",fix:"padding: crypto.constants.RSA_PKCS1_OAEP_PADDING",documentationLink:"https://nodejs.org/api/crypto.html#cryptopublicdecryptkey-buffer"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);const sourceCode=context.sourceCode;function checkCallExpression(node){if(isTestFile)return;const rsaMethods=new Set(["privateDecrypt","publicDecrypt","privateEncrypt","publicEncrypt"]);const isRsaCall=node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node.callee),rsaMethods)||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&rsaMethods.has(node.callee.name);if(isRsaCall&&node.arguments.length>=1){const keyArg=node.arguments[0];if(keyArg.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){for(const prop of keyArg.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&prop.key.name==="padding"){const paddingText=sourceCode.getText(prop.value);if(PKCS1_PADDING_NAMES.has(paddingText)||paddingText.includes("RSA_PKCS1_PADDING")){context.report({node:prop,messageId:"insecureRsaPadding",suggest:[{messageId:"useOaep",fix:fixer=>{return fixer.replaceText(prop.value,"crypto.constants.RSA_PKCS1_OAEP_PADDING")}}]})}}}}}}return{CallExpression:checkCallExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noMathRandomCrypto=void 0;const names_1=require("../../utils/names");const provenance_1=require("../../utils/provenance");const eslint_devkit_1=require("@interlace/eslint-devkit");const CSPRNG_CALLS=new Set(["getRandomValues","randomBytes","randomUUID","randomFillSync","randomFill","generateKey","subtle"]);const CRYPTO_WORDS=["token","tokens","key","keys","secret","secrets","password","passwd","salt","iv","nonce","seed","hash","cipher","auth","session","csrf","otp","pin","code","codes","verify","signature","credential","jwt","encryption","apikey","passphrase","mnemonic"];const DURATION_TAILS=new Set(["delay","timeout","interval","jitter","backoff","ms","millis","milliseconds","seconds","duration","wait","sleep","ttl","deadline","elapsed","latency","budget"]);const QUANTITY_TAILS=new Set(["count","counts","length","size","total","limit","quota","offset","index","rank","score","percent","ratio","rate","version","page"]);const NON_SECURITY_QUALIFIERS=new Map([["code",new Set(["http","status","error","err","exit","country","zip","postal","area","language","lang","locale","currency","promo","coupon","discount","color","colour","qr","bar","region","iso","mime","media","sort","source","char","unicode","ascii","airport"])],["key",new Set(["cache","map","object","row","index","partition","primary","foreign","sort","storage","translation","locale","i18n","react","idempotency","shortcut","keyboard","press","modifier"])]]);function nameSuggestsCrypto(name,vocab){if(!vocab.suggestsCrypto(name))return false;const words=(0,names_1.identifierWords)(name);const tail=words[words.length-1];if(DURATION_TAILS.has(tail)||QUANTITY_TAILS.has(tail))return false;const matched=words.filter(word=>vocab.words.has(word));if(matched.length===0)return true;return!matched.every(word=>{const qualifiers=NON_SECURITY_QUALIFIERS.get(word);return qualifiers!==void 0&&words.some(other=>qualifiers.has(other))})}function isMathRandomProperty(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.object.name!=="Math")return false;const property=node.property;if(!node.computed){return property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.name==="random"}return property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value==="random"}const NON_SECURITY_ID_QUALIFIERS=new Set(["request","req","correlation","trace","span","transaction","txn","message","msg","event","job","run","task","batch","element","node","row","record","instance","component","widget","dom","operation","call","invocation","frame","render"]);function isCorrelationIdFactory(name,vocab){const words=(0,names_1.identifierWords)(name);if(words[words.length-1]!=="id")return false;if(words.some(word=>vocab.words.has(word)))return false;return words.some(word=>vocab.correlationWords.has(word))}function functionNameSuggestsCrypto(name,vocab){if(!CRYPTO_FUNCTION_PATTERNS.some(pattern=>pattern.test(name)))return false;return!isCorrelationIdFactory(name,vocab)}const CRYPTO_FUNCTION_PATTERNS=[/generate.*token/i,/generate.*key/i,/generate.*id/i,/create.*secret/i,/create.*token/i,/random.*string/i,/get.*random.*(string|bytes|token|key|secret|value|id)/i,/make.*salt/i,/gen.*password/i];exports.noMathRandomCrypto=(0,eslint_devkit_1.createRule)({name:"no-math-random-crypto",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-math-random-crypto.md",description:"Disallow Math.random() for cryptographic purposes",cwe:"CWE-338",cvss:7.5,confidence:"medium"},messages:{pseudoRandomBytes:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Non-cryptographic random bytes",cwe:"CWE-338",owasp:"A02:2021",cvss:7.5,description:"crypto.pseudoRandomBytes() is not cryptographically secure. The name is the API's own warning: it was deprecated in Node 4 precisely because callers assumed otherwise.",severity:"HIGH",compliance:["SOC2","PCI-DSS","ISO27001"],fix:"Use crypto.randomBytes(n), or crypto.randomUUID() for identifiers.",documentationLink:"https://cwe.mitre.org/data/definitions/338.html"}),mathRandomCrypto:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Math.random() used for crypto",cwe:"CWE-338",description:"Math.random() is not cryptographically secure. It uses a PRNG that can be predicted. Never use it for tokens, keys, passwords, or any security-sensitive values.",severity:"CRITICAL",fix:"Use crypto.randomBytes() or crypto.randomUUID() instead",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true,description:"Allow Math.random() in test files. Default: true"},secretWords:{type:"array",items:{type:"string"},default:[...CRYPTO_WORDS],description:"Words that name a security value in this codebase. Replaces the default list."},correlationIdWords:{type:"array",items:{type:"string"},default:[...NON_SECURITY_ID_QUALIFIERS],description:"Words that mark a trailing `id` as a correlation id rather than a credential. Replaces the default list."}},additionalProperties:false}]},defaultOptions:[{allowInTests:true,secretWords:[...CRYPTO_WORDS],correlationIdWords:[...NON_SECURITY_ID_QUALIFIERS]}],create(context,[options={}]){const{allowInTests=true,secretWords=CRYPTO_WORDS,correlationIdWords=[...NON_SECURITY_ID_QUALIFIERS]}=options;const vocab={words:new Set(secretWords),correlationWords:new Set(correlationIdWords),suggestsCrypto:(0,names_1.makeNameTest)(secretWords)};const sourceCode=context.sourceCode;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function stableDeclarator(id){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(!variable||variable.defs.length!==1)return void 0;const def=variable.defs[0];if(def.type!=="Variable")return void 0;const reassigned=variable.references.some(reference=>reference.writeExpr!=null&&!reference.init);return reassigned?void 0:def.node}function isCsprngFallback(node){let scope=node.parent;while(scope!==void 0&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionExpression&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.Program){scope=scope.parent}let found=false;const stack=[scope];while(stack.length>0&&!found){const current=stack.pop();if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&current.range[1]<=node.range[0]){const callee=current.callee;const name=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property.name:callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:"";if(CSPRNG_CALLS.has(name))found=true}for(const key of Object.keys(current)){if(key==="parent")continue;const value=current[key];for(const child of Array.isArray(value)?value:[value]){if(child!==null&&typeof child==="object"&&typeof child.type==="string"){stack.push(child)}}}}return found}function isMathRandomCallee(callee){if(isMathRandomProperty(callee))return true;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init2=stableDeclarator(callee.object)?.init;if(!init2||init2.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return false;const wanted=callee.property.name;return init2.properties.some(property=>property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&!property.computed&&property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.key.name===wanted&&isMathRandomProperty(property.value))}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const declarator=stableDeclarator(callee);const init=declarator?.init;if(!declarator||!init)return false;if(declarator.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return isMathRandomProperty(init)}if(declarator.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return false;if(init.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||init.name!=="Math")return false;return declarator.id.properties.some(property=>{if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.computed)return false;if(property.value.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(property.value.name!==callee.name)return false;const key=property.key;if(key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return key.name==="random";return key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&key.value==="random"})}const MAX_BINDING_HOPS=2;function usedInCryptoContext(id,depth){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(!variable)return false;return variable.references.some(reference=>!reference.init&&isCryptoContext(reference.identifier,depth))}function escapesFunction(fn,cameFrom,passedReturn){if(fn.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&fn.body===cameFrom){return true}return passedReturn}function isCryptoContext(node,depth=0){let child=node;let current=node.parent;let namesThisValue=true;let passedReturn=false;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){if(!escapesFunction(current,child,passedReturn))namesThisValue=false;passedReturn=false}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement)passedReturn=true;if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator){if(current.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const varName=current.id.name;if(nameSuggestsCrypto(varName,vocab)){return true}if(depth<MAX_BINDING_HOPS&&usedInCryptoContext(current.id,depth+1)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&current.id){const funcName=current.id.name;if(functionNameSuggestsCrypto(funcName,vocab)){return true}}if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression){if(current.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&current.left.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const propName=current.left.property.name;if(nameSuggestsCrypto(propName,vocab)){return true}}if(current.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(nameSuggestsCrypto(current.left.name,vocab)){return true}}}if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.Property){if(current.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const propName=current.key.name;if(nameSuggestsCrypto(propName,vocab)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement){const func=findContainingFunction(current);if(func){if((func.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||func.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression)&&func.id?.name){const funcName=func.id.name;if(functionNameSuggestsCrypto(funcName,vocab)||nameSuggestsCrypto(funcName,vocab)){return true}}}}child=current;current=current.parent}return false}function findContainingFunction(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){return current}current=current.parent}return null}return{CallExpression(node){if(isTestFile)return;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.callee.computed&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.name==="pseudoRandomBytes"){context.report({node,messageId:"pseudoRandomBytes"});return}if(isMathRandomCallee(node.callee)){if(isCryptoContext(node)&&!isCsprngFallback(node)){context.report({node,messageId:"mathRandomCrypto"})}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noMathRandomCrypto=void 0;const names_1=require("../../utils/names");const provenance_1=require("../../utils/provenance");const eslint_devkit_1=require("@interlace/eslint-devkit");const CSPRNG_CALLS=new Set(["getRandomValues","randomBytes","randomUUID","randomFillSync","randomFill","generateKey","subtle"]);const CRYPTO_WORDS=["token","tokens","key","keys","secret","secrets","password","passwd","salt","iv","nonce","seed","hash","cipher","auth","session","csrf","otp","pin","code","codes","verify","signature","credential","jwt","encryption","apikey","passphrase","mnemonic"];const DURATION_TAILS=new Set(["delay","timeout","interval","jitter","backoff","ms","millis","milliseconds","seconds","duration","wait","sleep","ttl","deadline","elapsed","latency","budget"]);const QUANTITY_TAILS=new Set(["count","counts","length","size","total","limit","quota","offset","index","rank","score","percent","ratio","rate","version","page"]);const NON_SECURITY_QUALIFIERS=new Map([["code",new Set(["http","status","error","err","exit","country","zip","postal","area","language","lang","locale","currency","promo","coupon","discount","color","colour","qr","bar","region","iso","mime","media","sort","source","char","unicode","ascii","airport"])],["key",new Set(["cache","map","object","row","index","partition","primary","foreign","sort","storage","translation","locale","i18n","react","idempotency","shortcut","keyboard","press","modifier"])]]);function nameSuggestsCrypto(name,vocab){if(!vocab.suggestsCrypto(name))return false;const words=(0,names_1.identifierWords)(name);const tail=words[words.length-1];if(DURATION_TAILS.has(tail)||QUANTITY_TAILS.has(tail))return false;const matched=words.filter(word=>vocab.words.has(word));if(matched.length===0)return true;return!matched.every(word=>{const qualifiers=NON_SECURITY_QUALIFIERS.get(word);return qualifiers!==void 0&&words.some(other=>qualifiers.has(other))})}function isMathRandomProperty(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.object.name!=="Math")return false;const property=node.property;if(!node.computed){return property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.name==="random"}return property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value==="random"}const NON_SECURITY_ID_QUALIFIERS=new Set(["request","req","correlation","trace","span","transaction","txn","message","msg","event","job","run","task","batch","element","node","row","record","instance","component","widget","dom","operation","call","invocation","frame","render"]);function isCorrelationIdFactory(name,vocab){const words=(0,names_1.identifierWords)(name);if(words[words.length-1]!=="id")return false;if(words.some(word=>vocab.words.has(word)))return false;return words.some(word=>vocab.correlationWords.has(word))}function functionNameSuggestsCrypto(name,vocab){if(!CRYPTO_FUNCTION_PATTERNS.some(pattern=>pattern.test(name)))return false;return!isCorrelationIdFactory(name,vocab)}const CRYPTO_FUNCTION_PATTERNS=[/generate.*token/i,/generate.*key/i,/generate.*id/i,/create.*secret/i,/create.*token/i,/random.*string/i,/get.*random.*(string|bytes|token|key|secret|value|id)/i,/make.*salt/i,/gen.*password/i];exports.noMathRandomCrypto=(0,eslint_devkit_1.createRule)({name:"no-math-random-crypto",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-math-random-crypto.md",description:"Disallow Math.random() for cryptographic purposes",cwe:"CWE-338",cvss:7.5,confidence:"medium"},messages:{pseudoRandomBytes:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Non-cryptographic random bytes",cwe:"CWE-338",owasp:"A02:2021",cvss:7.5,description:"crypto.pseudoRandomBytes() is not cryptographically secure. The name is the API's own warning: it was deprecated in Node 4 precisely because callers assumed otherwise.",severity:"HIGH",compliance:["SOC2","PCI-DSS","ISO27001"],fix:"Use crypto.randomBytes(n), or crypto.randomUUID() for identifiers.",documentationLink:"https://cwe.mitre.org/data/definitions/338.html"}),mathRandomCrypto:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Math.random() used for crypto",cwe:"CWE-338",description:"Math.random() is not cryptographically secure. It uses a PRNG that can be predicted. Never use it for tokens, keys, passwords, or any security-sensitive values.",severity:"CRITICAL",fix:"Use crypto.randomBytes() or crypto.randomUUID() instead",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true,description:"Allow Math.random() in test files. Default: true"},secretWords:{type:"array",items:{type:"string"},default:[...CRYPTO_WORDS],description:"Words that name a security value in this codebase. Replaces the default list."},correlationIdWords:{type:"array",items:{type:"string"},default:[...NON_SECURITY_ID_QUALIFIERS],description:"Words that mark a trailing `id` as a correlation id rather than a credential. Replaces the default list."}},additionalProperties:false}]},defaultOptions:[{allowInTests:true,secretWords:[...CRYPTO_WORDS],correlationIdWords:[...NON_SECURITY_ID_QUALIFIERS]}],create(context,[options={}]){const{allowInTests=true,secretWords=CRYPTO_WORDS,correlationIdWords=[...NON_SECURITY_ID_QUALIFIERS]}=options;const vocab={words:new Set(secretWords),correlationWords:new Set(correlationIdWords),suggestsCrypto:(0,names_1.makeNameTest)(secretWords)};const sourceCode=context.sourceCode;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function stableDeclarator(id){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(!variable||variable.defs.length!==1)return void 0;const def=variable.defs[0];if(def.type!=="Variable")return void 0;const reassigned=variable.references.some(reference=>reference.writeExpr!=null&&!reference.init);return reassigned?void 0:def.node}function isCsprngFallback(node){let scope=node.parent;while(scope!==void 0&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionExpression&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&scope.type!==eslint_devkit_1.AST_NODE_TYPES.Program){scope=scope.parent}let found=false;const stack=[scope];while(stack.length>0&&!found){const current=stack.pop();if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&current.range[1]<=node.range[0]){const callee=current.callee;const name=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression?(0,eslint_devkit_1.propertyName)(callee)??"":callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:"";if(CSPRNG_CALLS.has(name))found=true}for(const key of Object.keys(current)){if(key==="parent")continue;const value=current[key];for(const child of Array.isArray(value)?value:[value]){if(child!==null&&typeof child==="object"&&typeof child.type==="string"){stack.push(child)}}}}return found}function isMathRandomCallee(callee){if(isMathRandomProperty(callee))return true;const wanted=(0,eslint_devkit_1.memberPropertyName)(callee);if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&wanted!==null){const init2=stableDeclarator(callee.object)?.init;if(!init2||init2.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return false;return init2.properties.some(property=>property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&(0,eslint_devkit_1.objectKeyName)(property)===wanted&&isMathRandomProperty(property.value))}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const declarator=stableDeclarator(callee);const init=declarator?.init;if(!declarator||!init)return false;if(declarator.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return isMathRandomProperty(init)}if(declarator.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return false;if(init.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||init.name!=="Math")return false;return declarator.id.properties.some(property=>{if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property||property.computed)return false;if(property.value.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(property.value.name!==callee.name)return false;const key=property.key;if(key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return key.name==="random";return key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&key.value==="random"})}const MAX_BINDING_HOPS=2;function usedInCryptoContext(id,depth){const variable=(0,provenance_1.findVariable)(sourceCode,id);if(!variable)return false;return variable.references.some(reference=>!reference.init&&isCryptoContext(reference.identifier,depth))}function escapesFunction(fn,cameFrom,passedReturn){if(fn.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&fn.body===cameFrom){return true}return passedReturn}function isCryptoContext(node,depth=0){let child=node;let current=node.parent;let namesThisValue=true;let passedReturn=false;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){if(!escapesFunction(current,child,passedReturn))namesThisValue=false;passedReturn=false}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement)passedReturn=true;if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator){if(current.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const varName=current.id.name;if(nameSuggestsCrypto(varName,vocab)){return true}if(depth<MAX_BINDING_HOPS&&usedInCryptoContext(current.id,depth+1)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&current.id){const funcName=current.id.name;if(functionNameSuggestsCrypto(funcName,vocab)){return true}}if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression){const propName=(0,eslint_devkit_1.memberPropertyName)(current.left);if(current.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&propName!==null){if(nameSuggestsCrypto(propName,vocab)){return true}}if(current.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(nameSuggestsCrypto(current.left.name,vocab)){return true}}}if(namesThisValue&&current.type===eslint_devkit_1.AST_NODE_TYPES.Property){if(current.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const propName=current.key.name;if(nameSuggestsCrypto(propName,vocab)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement){const func=findContainingFunction(current);if(func){if((func.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||func.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression)&&func.id?.name){const funcName=func.id.name;if(functionNameSuggestsCrypto(funcName,vocab)||nameSuggestsCrypto(funcName,vocab)){return true}}}}child=current;current=current.parent}return false}function findContainingFunction(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){return current}current=current.parent}return null}return{CallExpression(node){if(isTestFile)return;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(node.callee)==="pseudoRandomBytes"){context.report({node,messageId:"pseudoRandomBytes"});return}if(isMathRandomCallee(node.callee)){if(isCryptoContext(node)&&!isCsprngFallback(node)){context.report({node,messageId:"mathRandomCrypto"})}}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSelfSignedCerts=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noSelfSignedCerts=(0,eslint_devkit_1.createRule)({name:"no-self-signed-certs",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-self-signed-certs.md",description:"Disallow rejectUnauthorized: false in TLS options",cwe:"CWE-295",cvss:7.4},hasSuggestions:true,messages:{insecureTls:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"TLS certificate validation disabled",cwe:"CWE-295",description:"rejectUnauthorized: false disables TLS certificate validation, enabling man-in-the-middle attacks. Any certificate will be accepted, including self-signed and expired ones.",severity:"CRITICAL",fix:"Remove rejectUnauthorized: false or set it to true",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html"}),enableValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Enable certificate validation",description:"Enable proper TLS certificate validation",severity:"LOW",fix:"rejectUnauthorized: true (or remove the property)",documentationLink:"https://nodejs.org/api/tls.html#tlsconnectoptions-callback"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function checkProperty(node){if(isTestFile)return;if(node.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.key.name==="rejectUnauthorized"&&node.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&node.value.value===false){context.report({node,messageId:"insecureTls",suggest:[{messageId:"enableValidation",fix:fixer=>{return fixer.replaceText(node.value,"true")}}]})}}function checkAssignmentExpression(node){if(isTestFile)return;if(node.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.left.object.object.name==="process"&&node.left.object.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.left.object.property.name==="env"&&node.left.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.left.property.name==="NODE_TLS_REJECT_UNAUTHORIZED"){if(node.right.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&(node.right.value==="0"||node.right.value===0)){context.report({node,messageId:"insecureTls"})}}}return{Property:checkProperty,AssignmentExpression:checkAssignmentExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSelfSignedCerts=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noSelfSignedCerts=(0,eslint_devkit_1.createRule)({name:"no-self-signed-certs",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-self-signed-certs.md",description:"Disallow rejectUnauthorized: false in TLS options",cwe:"CWE-295",cvss:7.4},hasSuggestions:true,messages:{insecureTls:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"TLS certificate validation disabled",cwe:"CWE-295",description:"rejectUnauthorized: false disables TLS certificate validation, enabling man-in-the-middle attacks. Any certificate will be accepted, including self-signed and expired ones.",severity:"CRITICAL",fix:"Remove rejectUnauthorized: false or set it to true",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html"}),enableValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Enable certificate validation",description:"Enable proper TLS certificate validation",severity:"LOW",fix:"rejectUnauthorized: true (or remove the property)",documentationLink:"https://nodejs.org/api/tls.html#tlsconnectoptions-callback"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);function checkProperty(node){if(isTestFile)return;if(node.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.key.name==="rejectUnauthorized"&&node.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&node.value.value===false){context.report({node,messageId:"insecureTls",suggest:[{messageId:"enableValidation",fix:fixer=>{return fixer.replaceText(node.value,"true")}}]})}}function checkAssignmentExpression(node){if(isTestFile)return;if(node.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.object.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.left.object.object.name==="process"&&(0,eslint_devkit_1.propertyName)(node.left.object)==="env"&&(0,eslint_devkit_1.propertyName)(node.left)==="NODE_TLS_REJECT_UNAUTHORIZED"){if(node.right.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&(node.right.value==="0"||node.right.value===0)){context.report({node,messageId:"insecureTls"})}}}return{Property:checkProperty,AssignmentExpression:checkAssignmentExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noShellInjection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const constant_folding_1=require("../../utils/constant-folding");const SHELL_EXEC_FUNCTIONS=new Set(["exec","execSync"]);function isStringConcatOrTemplate(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral&&node.expressions.length>0)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"&&(node.left.type===eslint_devkit_1.AST_NODE_TYPES.Literal||node.left.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral||node.left.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression||node.right.type===eslint_devkit_1.AST_NODE_TYPES.Literal||node.right.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral))return true;return false}exports.noShellInjection=(0,eslint_devkit_1.createRule)({name:"no-shell-injection",skipTestFiles:true,meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-shell-injection.md",description:"Disallow string concatenation or template expressions in shell command arguments (CWE-78)",cwe:"CWE-78",cvss:9.8},messages:{shellInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"OS Command Injection (CWE-78)",cwe:"CWE-78",description:"Shell command built via string concatenation or template literal. An attacker who controls any interpolated value can execute arbitrary OS commands.",severity:"CRITICAL",fix:"Use spawn(cmd, [arg1, arg2]) with separate arguments instead of exec(cmd + args). Never build shell commands via string interpolation.",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html"})},schema:[{type:"object",properties:{requireModuleEvidence:{type:"boolean",default:true,description:"Only report when the callee resolves to child_process. Turning this off restores the pre-2026-08 behaviour, where any callee named exec/execSync/spawn was treated as a shell sink \u2014 which reported better-sqlite3 db.exec(sql) as CWE-78 at CVSS 9.8."}},additionalProperties:false}]},defaultOptions:[{requireModuleEvidence:true}],create(context,[options={}]){const isLiteralConstant=(0,constant_folding_1.makeIsLiteralConstant)(context.sourceCode);const{requireModuleEvidence=true}=options;return{CallExpression(node){const callee=node.callee;let fnName=null;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){fnName=callee.name}else if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){fnName=callee.property.name}if(!fnName||!SHELL_EXEC_FUNCTIONS.has(fnName))return;if(requireModuleEvidence&&!(0,eslint_devkit_1.isModuleBinding)(callee,context.sourceCode.getScope(node),"child_process")){return}const firstArg=node.arguments[0];if(!firstArg||firstArg.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return;if(!isStringConcatOrTemplate(firstArg))return;if(isLiteralConstant(firstArg))return;context.report({node:firstArg,messageId:"shellInjection"})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noShellInjection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const constant_folding_1=require("../../utils/constant-folding");const SHELL_EXEC_FUNCTIONS=new Set(["exec","execSync"]);function isStringConcatOrTemplate(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral&&node.expressions.length>0)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&node.operator==="+"&&(node.left.type===eslint_devkit_1.AST_NODE_TYPES.Literal||node.left.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral||node.left.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression||node.right.type===eslint_devkit_1.AST_NODE_TYPES.Literal||node.right.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral))return true;return false}exports.noShellInjection=(0,eslint_devkit_1.createRule)({name:"no-shell-injection",skipTestFiles:true,meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-shell-injection.md",description:"Disallow string concatenation or template expressions in shell command arguments (CWE-78)",cwe:"CWE-78",cvss:9.8},messages:{shellInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"OS Command Injection (CWE-78)",cwe:"CWE-78",description:"Shell command built via string concatenation or template literal. An attacker who controls any interpolated value can execute arbitrary OS commands.",severity:"CRITICAL",fix:"Use spawn(cmd, [arg1, arg2]) with separate arguments instead of exec(cmd + args). Never build shell commands via string interpolation.",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html"})},schema:[{type:"object",properties:{requireModuleEvidence:{type:"boolean",default:true,description:"Only report when the callee resolves to child_process. Turning this off restores the pre-2026-08 behaviour, where any callee named exec/execSync/spawn was treated as a shell sink \u2014 which reported better-sqlite3 db.exec(sql) as CWE-78 at CVSS 9.8."}},additionalProperties:false}]},defaultOptions:[{requireModuleEvidence:true}],create(context,[options={}]){const isLiteralConstant=(0,constant_folding_1.makeIsLiteralConstant)(context.sourceCode);const{requireModuleEvidence=true}=options;return{CallExpression(node){const callee=node.callee;let fnName=null;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){fnName=callee.name}else if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){fnName=(0,eslint_devkit_1.propertyName)(callee)}if(!fnName||!SHELL_EXEC_FUNCTIONS.has(fnName))return;if(requireModuleEvidence&&!(0,eslint_devkit_1.isModuleBinding)(callee,context.sourceCode.getScope(node),"child_process")){return}const firstArg=node.arguments[0];if(!firstArg||firstArg.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return;if(!isStringConcatOrTemplate(firstArg))return;if(isLiteralConstant(firstArg))return;context.report({node:firstArg,messageId:"shellInjection"})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSsrf=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const HTTP_CLIENT_FUNCTIONS=new Set(["fetch","got","nodeFetch","undici"]);const HTTP_CLIENT_METHODS=new Set(["get","post","put","patch","delete","head","options","request"]);const HTTP_CLIENT_MODULES=["axios","got","superagent","request","http","https","node:http","node:https","undici","needle"];const VALIDATION_FUNCTION_NAMES=new Set(["validateUrl","validateURL","isValidUrl","isSafeUrl","isAllowed","isValidURL","checkUrl","checkURL","sanitizeUrl","sanitizeURL"]);const USER_INPUT_SUBSTRINGS=["url","endpoint","uri","href","link","target","dest","source","host","user","input","param"];function isUserInputParamName(name){const lower=name.toLowerCase();return USER_INPUT_SUBSTRINGS.some(sub=>lower.includes(sub))}const REQUEST_ROOT_NAMES=new Set(["req","request","ctx","event"]);const URL_OPTION_KEYS=new Set(["url","href","uri"]);function isRequestSourced(node,sourceCode){if((0,eslint_devkit_1.readsRequestShape)(node,sourceCode))return true;let current=node;while(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object}return current!==node&&current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&REQUEST_ROOT_NAMES.has(current.name.toLowerCase())}function makeCarriesUntrustedUrl(sourceCode,reportUnresolvedUrls){const carries=(node,depth)=>{if(depth>6)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return carries(bare,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const init=(0,provenance_1.bindingInit)(sourceCode,node);if(init!==void 0)return carries(init,depth+1);return reportUnresolvedUrls&&isUserInputParamName(node.name)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isRequestSourced(node,sourceCode)||carries(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>carries(expression,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(argument=>carries(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return carries(node.left,depth+1)||carries(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.ObjectExpression:return node.properties.some(property=>{if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property)return false;const value=property.value;if(isRequestSourced(value,sourceCode))return true;const key=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:property.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal?String(property.key.value):"";return URL_OPTION_KEYS.has(key.toLowerCase())&&carries(value,depth+1)});default:return false}};return node=>carries(node,0)}function nodeContainsValidation(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="URL"){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&VALIDATION_FUNCTION_NAMES.has(node.callee.name)){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const method=node.callee.property.name;if(method==="includes"||method==="has"||method==="startsWith"||method==="test"||method==="some"){return true}}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&(node.operator==="==="||node.operator==="==")&&(node.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.left.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.left.property.name==="hostname"||node.left.property.name==="host")||node.right.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.right.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.right.property.name==="hostname"||node.right.property.name==="host"))){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement){return true}const SKIP_KEYS=new Set(["parent","range","loc","tokens","comments","start","end"]);for(const key of Object.keys(node)){if(SKIP_KEYS.has(key))continue;const value=node[key];if(value&&typeof value==="object"&&"type"in value){if(nodeContainsValidation(value))return true}if(Array.isArray(value)){for(const item of value){if(item&&typeof item==="object"&&"type"in item){if(nodeContainsValidation(item))return true}}}}return false}function hasValidationBefore(node){let current=node.parent;while(current){const parent=current.parent;if(!parent)break;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement||parent.type===eslint_devkit_1.AST_NODE_TYPES.Program){const body=parent.body;const idx=body.indexOf(current);for(let i=idx-1;i>=0&&i>=idx-10;i--){if(nodeContainsValidation(body[i])){return true}}}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&parent.test){if(nodeContainsValidation(parent.test)){return true}}current=parent}return false}exports.noSsrf=(0,eslint_devkit_1.createRule)({name:"no-ssrf",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-ssrf.md",description:"Flags HTTP calls whose URL argument is a user-input-named identifier or reads off a request object \u2014 a heuristic prompt for code review, not a proof of SSRF",cwe:"CWE-918",cvss:9.1},messages:{ssrfVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Possible SSRF \u2014 heuristic (CWE-918)",cwe:"CWE-918",description:"HTTP call whose URL argument name suggests user input. This is a naming heuristic, not data-flow analysis \u2014 review whether the URL could originate from an untrusted source at runtime.",severity:"LOW",fix:"If the URL comes from user input, validate it against an allowlist of permitted hosts before making the request.",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true},reportUnresolvedUrls:{type:"boolean",default:false,description:"Report a URL argument that is a user-input-named identifier with no traceable request source. Restores the pre-inversion naming heuristic."}},additionalProperties:false}]},defaultOptions:[{allowInTests:true}],create(context,[options={}]){const{allowInTests=true,reportUnresolvedUrls=false}=options||{};const carriesUntrustedUrl=makeCarriesUntrustedUrl(context.sourceCode,reportUnresolvedUrls);const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);if(isTestFile)return{};return{CallExpression(node){let isHttpCall=false;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&HTTP_CLIENT_FUNCTIONS.has(node.callee.name)){isHttpCall=true}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&HTTP_CLIENT_METHODS.has(node.callee.property.name)&&HTTP_CLIENT_MODULES.some(mod=>(0,eslint_devkit_1.isModuleBinding)(node.callee.object,context.sourceCode.getScope(node),mod))){isHttpCall=true}if(!isHttpCall)return;const urlArg=node.arguments[0];if(!urlArg)return;if(hasValidationBefore(node)){return}if(!carriesUntrustedUrl(urlArg)){return}context.report({node,messageId:"ssrfVulnerability"})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSsrf=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const HTTP_CLIENT_FUNCTIONS=new Set(["fetch","got","nodeFetch","undici"]);const HTTP_CLIENT_METHODS=new Set(["get","post","put","patch","delete","head","options","request"]);const HTTP_CLIENT_MODULES=["axios","got","superagent","request","http","https","node:http","node:https","undici","needle"];const VALIDATION_FUNCTION_NAMES=new Set(["validateUrl","validateURL","isValidUrl","isSafeUrl","isAllowed","isValidURL","checkUrl","checkURL","sanitizeUrl","sanitizeURL"]);const USER_INPUT_SUBSTRINGS=["url","endpoint","uri","href","link","target","dest","source","host","user","input","param"];function isUserInputParamName(name){const lower=name.toLowerCase();return USER_INPUT_SUBSTRINGS.some(sub=>lower.includes(sub))}const REQUEST_ROOT_NAMES=new Set(["req","request","ctx","event"]);const URL_OPTION_KEYS=new Set(["url","href","uri"]);function isRequestSourced(node,sourceCode){if((0,eslint_devkit_1.readsRequestShape)(node,sourceCode))return true;let current=node;while(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object}return current!==node&&current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&REQUEST_ROOT_NAMES.has(current.name.toLowerCase())}function makeCarriesUntrustedUrl(sourceCode,reportUnresolvedUrls){const carries=(node,depth)=>{if(depth>6)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return carries(bare,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const init=(0,provenance_1.bindingInit)(sourceCode,node);if(init!==void 0)return carries(init,depth+1);return reportUnresolvedUrls&&isUserInputParamName(node.name)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isRequestSourced(node,sourceCode)||carries(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>carries(expression,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(argument=>carries(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return carries(node.left,depth+1)||carries(node.right,depth+1);case eslint_devkit_1.AST_NODE_TYPES.ObjectExpression:return node.properties.some(property=>{if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property)return false;const value=property.value;if(isRequestSourced(value,sourceCode))return true;const key=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:property.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal?String(property.key.value):"";return URL_OPTION_KEYS.has(key.toLowerCase())&&carries(value,depth+1)});default:return false}};return node=>carries(node,0)}function nodeContainsValidation(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="URL"){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&VALIDATION_FUNCTION_NAMES.has(node.callee.name)){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const method=node.callee.property.name;if(method==="includes"||method==="has"||method==="startsWith"||method==="test"||method==="some"){return true}}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&(node.operator==="==="||node.operator==="==")&&(node.left.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&((0,eslint_devkit_1.propertyName)(node.left)==="hostname"||(0,eslint_devkit_1.propertyName)(node.left)==="host")||node.right.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&((0,eslint_devkit_1.propertyName)(node.right)==="hostname"||(0,eslint_devkit_1.propertyName)(node.right)==="host"))){return true}if(node.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement){return true}const SKIP_KEYS=new Set(["parent","range","loc","tokens","comments","start","end"]);for(const key of Object.keys(node)){if(SKIP_KEYS.has(key))continue;const value=node[key];if(value&&typeof value==="object"&&"type"in value){if(nodeContainsValidation(value))return true}if(Array.isArray(value)){for(const item of value){if(item&&typeof item==="object"&&"type"in item){if(nodeContainsValidation(item))return true}}}}return false}function hasValidationBefore(node){let current=node.parent;while(current){const parent=current.parent;if(!parent)break;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement||parent.type===eslint_devkit_1.AST_NODE_TYPES.Program){const body=parent.body;const idx=body.indexOf(current);for(let i=idx-1;i>=0&&i>=idx-10;i--){if(nodeContainsValidation(body[i])){return true}}}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&parent.test){if(nodeContainsValidation(parent.test)){return true}}current=parent}return false}exports.noSsrf=(0,eslint_devkit_1.createRule)({name:"no-ssrf",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-ssrf.md",description:"Flags HTTP calls whose URL argument is a user-input-named identifier or reads off a request object \u2014 a heuristic prompt for code review, not a proof of SSRF",cwe:"CWE-918",cvss:9.1},messages:{ssrfVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Possible SSRF \u2014 heuristic (CWE-918)",cwe:"CWE-918",description:"HTTP call whose URL argument name suggests user input. This is a naming heuristic, not data-flow analysis \u2014 review whether the URL could originate from an untrusted source at runtime.",severity:"LOW",fix:"If the URL comes from user input, validate it against an allowlist of permitted hosts before making the request.",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true},reportUnresolvedUrls:{type:"boolean",default:false,description:"Report a URL argument that is a user-input-named identifier with no traceable request source. Restores the pre-inversion naming heuristic."}},additionalProperties:false}]},defaultOptions:[{allowInTests:true}],create(context,[options={}]){const{allowInTests=true,reportUnresolvedUrls=false}=options||{};const carriesUntrustedUrl=makeCarriesUntrustedUrl(context.sourceCode,reportUnresolvedUrls);const filename=context.filename;const isTestFile=allowInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);if(isTestFile)return{};return{CallExpression(node){let isHttpCall=false;if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&HTTP_CLIENT_FUNCTIONS.has(node.callee.name)){isHttpCall=true}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node.callee),HTTP_CLIENT_METHODS)&&HTTP_CLIENT_MODULES.some(mod=>(0,eslint_devkit_1.isModuleBinding)(node.callee.object,context.sourceCode.getScope(node),mod))){isHttpCall=true}if(!isHttpCall)return;const urlArg=node.arguments[0];if(!urlArg)return;if(hasValidationBefore(node)){return}if(!carriesUntrustedUrl(urlArg)){return}context.report({node,messageId:"ssrfVulnerability"})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const names_1=require("../../utils/names");const DEFAULT_SECRET_PATTERNS=["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];function isSourceConstant(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.name==="undefined")return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return node.expressions.length===0;return false}const BOOLEAN_PREDICATE_NAME=/^(?:is|has|should|can|did|was|will|does)[A-Z]/;const NAMED_CONSTANT=/^[A-Z][A-Z0-9_]*$/;const DEFAULT_UNTRUSTED_SOURCES=["req","request","ctx","event"];const NAMESPACE_NAME=/^(?:[A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)$/;function isNamedConstant(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.computed)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return NAMESPACE_NAME.test(node.object.name)&&NAMED_CONSTANT.test(node.property.name)}function memberRoot(node){let current=node;for(;;){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object;continue}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){current=current.callee;continue}break}return current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?current:null}function isSelfComparison(left,right){const pair=(bare,derived)=>{if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(derived.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return memberRoot(derived)?.name===bare.name};return pair(left,right)||pair(right,left)}const NON_SECRET_MEMBERS=new Map([["hash",new Set(["location"])]]);const DEFAULT_NON_SECRET_WORDS=["author","authors","authored","authoring","authorship","hashtag","hashtags"];const DEFAULT_NON_SECRET_TAILS=["count","counts","limit","limits","usage","total","size","length","price","cost","quota","address","addresses","index","rank","percent","path","paths","pathname","pathnames","endpoint","endpoints","route","routes","hostname","host","port","origin","kind","kinds","type","types","flag","flags","category"];const CRYPTO_DERIVATIONS=new Set(["createHmac","createHash","createSign","pbkdf2Sync","scryptSync","hkdfSync","digest","sign","hmac"]);const RECEIVER_COMPARE_METHODS=new Set(["equals","startsWith","endsWith","localeCompare"]);const BINARY_EQUALITY_FUNCTIONS=new Set(["isEqual","isEqualWith","deepEqual","fastDeepEqual","shallowEqual"]);const SERVER_STATE_REQUEST_PROPERTIES=new Set(["session","user","locals","app","state"]);function unwrapChain(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.ChainExpression?node.expression:node}function memberPropertyName(node){const property=node.property;if(!node.computed){return property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.name:null}return(0,eslint_devkit_1.staticString)(property)!==null?(0,eslint_devkit_1.staticString)(property):null}function isNonSecretMember(node){const propertyName=memberPropertyName(node);if(propertyName===null)return false;const receivers=NON_SECRET_MEMBERS.get(propertyName.toLowerCase());if(!receivers)return false;const owner=node.object;const ownerName=owner.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.name:owner.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!owner.computed&&owner.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.property.name:"";return receivers.has(ownerName.toLowerCase())}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:5.9},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"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"},untrustedSources:{type:"array",items:{type:"string"},default:DEFAULT_UNTRUSTED_SOURCES,description:"Identifier roots treated as attacker-controlled (default: req, request, ctx, event)"},reportUnverifiedComparisons:{type:"boolean",default:false,description:"Report on a secret-looking name alone, without an attacker-controlled operand. Restores the pre-inversion behaviour."},nonSecretWords:{type:"array",items:{type:"string"},default:[...DEFAULT_NON_SECRET_WORDS],description:`Whole words that mean a secretPatterns match was a collision (default: ${DEFAULT_NON_SECRET_WORDS.join(", ")}). Replaces the list.`},nonSecretTails:{type:"array",items:{type:"string"},default:[...DEFAULT_NON_SECRET_TAILS],description:`Trailing words that make the value a measurement or location rather than a credential (default: ${DEFAULT_NON_SECRET_TAILS.join(", ")}). Replaces the list.`}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS,nonSecretWords:[...DEFAULT_NON_SECRET_WORDS],nonSecretTails:[...DEFAULT_NON_SECRET_TAILS]}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS,untrustedSources=DEFAULT_UNTRUSTED_SOURCES,reportUnverifiedComparisons=false,nonSecretWords=[...DEFAULT_NON_SECRET_WORDS],nonSecretTails=[...DEFAULT_NON_SECRET_TAILS]}=options;const nonSecretWordSet=new Set(nonSecretWords.map(word=>word.toLowerCase()));const nonSecretTailSet=new Set(nonSecretTails.map(word=>word.toLowerCase()));const sourceCode=context.sourceCode;const readsUntrusted=(0,provenance_1.makeReadsTaintSource)(sourceCode,new Set(untrustedSources.map(source=>source.toLowerCase())));const patterns=(0,eslint_devkit_1.compileUserPatterns)(secretPatterns,"i");function nameLooksSecret(name){if(BOOLEAN_PREDICATE_NAME.test(name))return false;const words=(0,names_1.identifierWords)(name);if(words.some(word=>nonSecretWordSet.has(word)))return false;if(words.length>0&&nonSecretTailSet.has(words[words.length-1])){return false}return patterns.some(p=>p.test(name))}function isCryptoDerivation(node){const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return isCryptoDerivation(bare);if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return CRYPTO_DERIVATIONS.has(callee.name)}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(CRYPTO_DERIVATIONS.has(callee.property.name))return true;return isCryptoDerivation(callee.object)}function isCryptoSecret(node){if(isCryptoDerivation(node))return true;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const init=(0,provenance_1.bindingInit)(sourceCode,bare);return init!==void 0&&containsCryptoDerivation(init)}function containsCryptoDerivation(node){if(isCryptoDerivation(node))return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.expressions.some(expression=>containsCryptoDerivation(expression))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return containsCryptoDerivation(node.left)||containsCryptoDerivation(node.right)}return false}function isServerDerived(node,depth=0){if(depth>4)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return isServerDerived(bare,depth+1);if(node.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression)return true;if(containsCryptoDerivation(node))return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const propertyName=memberPropertyName(node);if(propertyName!==null&&SERVER_STATE_REQUEST_PROPERTIES.has(propertyName)){return true}return isServerDerived(node.object,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,provenance_1.bindingInit)(sourceCode,node);return init!==void 0&&isServerDerived(init,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=unwrapChain(node.callee);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;return!readsUntrusted(callee.object)||isServerDerived(callee.object,depth+1)}return false}function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(node.name)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){if(isNonSecretMember(node))return false;const propertyName=memberPropertyName(node);if(propertyName!==null){return nameLooksSecret(propertyName)}}return false}function isResolvedConstant(node){if(isSourceConstant(node))return true;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return(0,provenance_1.constLiteralOf)(sourceCode,node)!==void 0}function resolveLocalFunction(callee){const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type==="FunctionName")return def.node;if(def.type!=="Variable")return null;const init=def.node.init;if(!init)return null;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(init);if(bare.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression||bare.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression){return bare}return null}function comparesLengths(node){const isLength=operand=>operand.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&memberPropertyName(operand)==="length";return isLength(node.left)||isLength(node.right)}function equalityWrapperParams(callee){const fn=resolveLocalFunction(callee);if(!fn||fn.params.length<2)return null;const paramIndex=new Map;fn.params.forEach((param,index)=>{if(param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)paramIndex.set(param.name,index)});let found=null;const visit=node=>{if(found||!node||typeof node.type!=="string")return;if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){const isEquality=node.operator==="==="||node.operator==="!=="||node.operator==="=="||node.operator==="!=";if(isEquality&&!comparesLengths(node)){const leftIndex=paramIndex.get(memberRoot(node.left)?.name??"");const rightIndex=paramIndex.get(memberRoot(node.right)?.name??"");if(leftIndex!==void 0&&rightIndex!==void 0&&leftIndex!==rightIndex){found=[leftIndex,rightIndex];return}}}for(const[key,value]of Object.entries(node)){if(key==="parent")continue;if(Array.isArray(value)){for(const entry of value)visit(entry)}else if(value&&typeof value==="object"&&"type"in value){visit(value)}}};visit(fn.body);return found}function comparisonOperands(node){const args=node.arguments;if(args.some(argument=>argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)){return null}const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(BINARY_EQUALITY_FUNCTIONS.has(callee.name)&&args.length===2){return[args[0],args[1]]}const wrapped=equalityWrapperParams(callee);if(!wrapped)return null;const[leftIndex,rightIndex]=wrapped;const leftArg=args[leftIndex];const rightArg=args[rightIndex];if(!leftArg||!rightArg)return null;return[leftArg,rightArg]}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed)return null;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const method=callee.property.name;if(method==="compare"&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="Buffer"&&args.length===2){return[args[0],args[1]]}if(BINARY_EQUALITY_FUNCTIONS.has(method)&&args.length===2){return[args[0],args[1]]}if(RECEIVER_COMPARE_METHODS.has(method)&&args.length===1){return[callee.object,args[0]]}return null}function checkComparison(node,rawLeft,rawRight){const left=unwrapChain(rawLeft);const right=unwrapChain(rawRight);if(isResolvedConstant(left)||isResolvedConstant(right)){return}if(isNamedConstant(left)||isNamedConstant(right)){return}if(isSelfComparison(left,right)){return}const leftIsSecret=isSecretIdentifier(left)||isCryptoSecret(left);const rightIsSecret=isSecretIdentifier(right)||isCryptoSecret(right);if(leftIsSecret||rightIsSecret){if(!reportUnverifiedComparisons){const leftUntrusted=readsUntrusted(left);const rightUntrusted=readsUntrusted(right);if(leftUntrusted===rightUntrusted){if(!leftUntrusted)return;if(!isServerDerived(left)&&!isServerDerived(right))return}}context.report({node,messageId:"timingUnsafeCompare"})}}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}checkComparison(node,node.left,node.right)}function checkCallExpression(node){const operands=comparisonOperands(node);if(!operands)return;checkComparison(node,operands[0],operands[1])}return{BinaryExpression:checkBinaryExpression,CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const names_1=require("../../utils/names");const DEFAULT_SECRET_PATTERNS=["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];function isSourceConstant(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.name==="undefined")return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return node.expressions.length===0;return false}const BOOLEAN_PREDICATE_NAME=/^(?:is|has|should|can|did|was|will|does)[A-Z]/;const NAMED_CONSTANT=/^[A-Z][A-Z0-9_]*$/;const DEFAULT_UNTRUSTED_SOURCES=["req","request","ctx","event"];const NAMESPACE_NAME=/^(?:[A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)$/;function isNamedConstant(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const constant=(0,eslint_devkit_1.propertyName)(node);if(constant===null)return false;return NAMESPACE_NAME.test(node.object.name)&&NAMED_CONSTANT.test(constant)}function memberRoot(node){let current=node;for(;;){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object;continue}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){current=current.callee;continue}break}return current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?current:null}function isSelfComparison(left,right){const pair=(bare,derived)=>{if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(derived.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return memberRoot(derived)?.name===bare.name};return pair(left,right)||pair(right,left)}const NON_SECRET_MEMBERS=new Map([["hash",new Set(["location"])]]);const DEFAULT_NON_SECRET_WORDS=["author","authors","authored","authoring","authorship","hashtag","hashtags"];const DEFAULT_NON_SECRET_TAILS=["count","counts","limit","limits","usage","total","size","length","price","cost","quota","address","addresses","index","rank","percent","path","paths","pathname","pathnames","endpoint","endpoints","route","routes","hostname","host","port","origin","kind","kinds","type","types","flag","flags","category"];const CRYPTO_DERIVATIONS=new Set(["createHmac","createHash","createSign","pbkdf2Sync","scryptSync","hkdfSync","digest","sign","hmac"]);const RECEIVER_COMPARE_METHODS=new Set(["equals","startsWith","endsWith","localeCompare"]);const BINARY_EQUALITY_FUNCTIONS=new Set(["isEqual","isEqualWith","deepEqual","fastDeepEqual","shallowEqual"]);const SERVER_STATE_REQUEST_PROPERTIES=new Set(["session","user","locals","app","state"]);function unwrapChain(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.ChainExpression?node.expression:node}function memberPropertyName(node){const property=node.property;if(!node.computed){return property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.name:null}return(0,eslint_devkit_1.staticString)(property)!==null?(0,eslint_devkit_1.staticString)(property):null}function isNonSecretMember(node){const member=memberPropertyName(node);if(member===null)return false;const receivers=NON_SECRET_MEMBERS.get(member.toLowerCase());if(!receivers)return false;const owner=node.object;const ownerName=owner.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.name:owner.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression?(0,eslint_devkit_1.propertyName)(owner)??"":"";return receivers.has(ownerName.toLowerCase())}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:5.9},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"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"},untrustedSources:{type:"array",items:{type:"string"},default:DEFAULT_UNTRUSTED_SOURCES,description:"Identifier roots treated as attacker-controlled (default: req, request, ctx, event)"},reportUnverifiedComparisons:{type:"boolean",default:false,description:"Report on a secret-looking name alone, without an attacker-controlled operand. Restores the pre-inversion behaviour."},nonSecretWords:{type:"array",items:{type:"string"},default:[...DEFAULT_NON_SECRET_WORDS],description:`Whole words that mean a secretPatterns match was a collision (default: ${DEFAULT_NON_SECRET_WORDS.join(", ")}). Replaces the list.`},nonSecretTails:{type:"array",items:{type:"string"},default:[...DEFAULT_NON_SECRET_TAILS],description:`Trailing words that make the value a measurement or location rather than a credential (default: ${DEFAULT_NON_SECRET_TAILS.join(", ")}). Replaces the list.`}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS,nonSecretWords:[...DEFAULT_NON_SECRET_WORDS],nonSecretTails:[...DEFAULT_NON_SECRET_TAILS]}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS,untrustedSources=DEFAULT_UNTRUSTED_SOURCES,reportUnverifiedComparisons=false,nonSecretWords=[...DEFAULT_NON_SECRET_WORDS],nonSecretTails=[...DEFAULT_NON_SECRET_TAILS]}=options;const nonSecretWordSet=new Set(nonSecretWords.map(word=>word.toLowerCase()));const nonSecretTailSet=new Set(nonSecretTails.map(word=>word.toLowerCase()));const sourceCode=context.sourceCode;const readsUntrusted=(0,provenance_1.makeReadsTaintSource)(sourceCode,new Set(untrustedSources.map(source=>source.toLowerCase())));const patterns=(0,eslint_devkit_1.compileUserPatterns)(secretPatterns,"i");function nameLooksSecret(name){if(BOOLEAN_PREDICATE_NAME.test(name))return false;const words=(0,names_1.identifierWords)(name);if(words.some(word=>nonSecretWordSet.has(word)))return false;if(words.length>0&&nonSecretTailSet.has(words[words.length-1])){return false}return patterns.some(p=>p.test(name))}function isCryptoDerivation(node){const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return isCryptoDerivation(bare);if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return CRYPTO_DERIVATIONS.has(callee.name)}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed)return false;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(CRYPTO_DERIVATIONS.has(callee.property.name))return true;return isCryptoDerivation(callee.object)}function isCryptoSecret(node){if(isCryptoDerivation(node))return true;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const init=(0,provenance_1.bindingInit)(sourceCode,bare);return init!==void 0&&containsCryptoDerivation(init)}function containsCryptoDerivation(node){if(isCryptoDerivation(node))return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.expressions.some(expression=>containsCryptoDerivation(expression))}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return containsCryptoDerivation(node.left)||containsCryptoDerivation(node.right)}return false}function isServerDerived(node,depth=0){if(depth>4)return false;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(node);if(bare!==node)return isServerDerived(bare,depth+1);if(node.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression)return true;if(containsCryptoDerivation(node))return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const propertyName=memberPropertyName(node);if(propertyName!==null&&SERVER_STATE_REQUEST_PROPERTIES.has(propertyName)){return true}return isServerDerived(node.object,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const init=(0,provenance_1.bindingInit)(sourceCode,node);return init!==void 0&&isServerDerived(init,depth+1)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=unwrapChain(node.callee);if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;return!readsUntrusted(callee.object)||isServerDerived(callee.object,depth+1)}return false}function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(node.name)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){if(isNonSecretMember(node))return false;const propertyName=memberPropertyName(node);if(propertyName!==null){return nameLooksSecret(propertyName)}}return false}function isResolvedConstant(node){if(isSourceConstant(node))return true;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return(0,provenance_1.constLiteralOf)(sourceCode,node)!==void 0}function resolveLocalFunction(callee){const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type==="FunctionName")return def.node;if(def.type!=="Variable")return null;const init=def.node.init;if(!init)return null;const bare=(0,eslint_devkit_1.unwrapTypeSyntax)(init);if(bare.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression||bare.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression){return bare}return null}function comparesLengths(node){const isLength=operand=>operand.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&memberPropertyName(operand)==="length";return isLength(node.left)||isLength(node.right)}function equalityWrapperParams(callee){const fn=resolveLocalFunction(callee);if(!fn||fn.params.length<2)return null;const paramIndex=new Map;fn.params.forEach((param,index)=>{if(param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)paramIndex.set(param.name,index)});let found=null;const visit=node=>{if(found||!node||typeof node.type!=="string")return;if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){const isEquality=node.operator==="==="||node.operator==="!=="||node.operator==="=="||node.operator==="!=";if(isEquality&&!comparesLengths(node)){const leftIndex=paramIndex.get(memberRoot(node.left)?.name??"");const rightIndex=paramIndex.get(memberRoot(node.right)?.name??"");if(leftIndex!==void 0&&rightIndex!==void 0&&leftIndex!==rightIndex){found=[leftIndex,rightIndex];return}}}for(const[key,value]of Object.entries(node)){if(key==="parent")continue;if(Array.isArray(value)){for(const entry of value)visit(entry)}else if(value&&typeof value==="object"&&"type"in value){visit(value)}}};visit(fn.body);return found}function comparisonOperands(node){const args=node.arguments;if(args.some(argument=>argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)){return null}const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){if(BINARY_EQUALITY_FUNCTIONS.has(callee.name)&&args.length===2){return[args[0],args[1]]}const wrapped=equalityWrapperParams(callee);if(!wrapped)return null;const[leftIndex,rightIndex]=wrapped;const leftArg=args[leftIndex];const rightArg=args[rightIndex];if(!leftArg||!rightArg)return null;return[leftArg,rightArg]}if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;const method=(0,eslint_devkit_1.propertyName)(callee);if(method===null)return null;if(method==="compare"&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="Buffer"&&args.length===2){return[args[0],args[1]]}if(BINARY_EQUALITY_FUNCTIONS.has(method)&&args.length===2){return[args[0],args[1]]}if(RECEIVER_COMPARE_METHODS.has(method)&&args.length===1){return[callee.object,args[0]]}return null}function checkComparison(node,rawLeft,rawRight){const left=unwrapChain(rawLeft);const right=unwrapChain(rawRight);if(isResolvedConstant(left)||isResolvedConstant(right)){return}if(isNamedConstant(left)||isNamedConstant(right)){return}if(isSelfComparison(left,right)){return}const leftIsSecret=isSecretIdentifier(left)||isCryptoSecret(left);const rightIsSecret=isSecretIdentifier(right)||isCryptoSecret(right);if(leftIsSecret||rightIsSecret){if(!reportUnverifiedComparisons){const leftUntrusted=readsUntrusted(left);const rightUntrusted=readsUntrusted(right);if(leftUntrusted===rightUntrusted){if(!leftUntrusted)return;if(!isServerDerived(left)&&!isServerDerived(right))return}}context.report({node,messageId:"timingUnsafeCompare"})}}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}checkComparison(node,node.left,node.right)}function checkCallExpression(node){const operands=comparisonOperands(node);if(!operands)return;checkComparison(node,operands[0],operands[1])}return{BinaryExpression:checkBinaryExpression,CallExpression:checkCallExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noToctouVulnerability=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const PER_USER_ROOT_FUNCTIONS=new Set(["homedir","userInfo"]);const PER_USER_ENV_VARS=new Set(["HOME","USERPROFILE","LOCALAPPDATA","APPDATA","XDG_CACHE_HOME","XDG_CONFIG_HOME","XDG_DATA_HOME","XDG_STATE_HOME"]);const DEFAULT_FS_METHODS=["fs.existsSync","fs.statSync","fs.accessSync","fs.exists","fs.stat","fs.access"];const MKDIR_METHODS=new Set(["mkdir","mkdirSync"]);const RISKY_USE_METHODS=new Set(["readFileSync","readFile","openSync","open","createReadStream","writeFileSync","writeFile","appendFileSync","appendFile","createWriteStream","unlinkSync","unlink","rmSync","rm","rmdirSync","rmdir","mkdirSync","mkdir","renameSync","rename","copyFileSync","copyFile","truncateSync","truncate","chmodSync","chmod","chownSync","chown","symlinkSync","symlink","linkSync","link"]);function memberName(callee){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;if(callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!callee.computed){return callee.property.name}return callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof callee.property.value==="string"?callee.property.value:null}exports.noToctouVulnerability=(0,eslint_devkit_2.createRule)({name:"no-toctou-vulnerability",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-toctou-vulnerability.md",description:"Detects Time-of-Check-Time-of-Use vulnerabilities",cwe:"CWE-367",cvss:7},messages:{toctouVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"TOCTOU vulnerability",cwe:"CWE-367",description:"Time-of-check Time-of-use race condition detected",severity:"HIGH",fix:"Act on the result and handle the failure \u2014 open/unlink and catch ENOENT \u2014 instead of checking first. Not a finding if the path is inside a directory only this user can write",documentationLink:"https://cwe.mitre.org/data/definitions/367.html"})},schema:[{type:"object",properties:{ignoreInTests:{type:"boolean",default:true},fsMethods:{type:"array",items:{type:"string"},default:DEFAULT_FS_METHODS,description:"Filesystem check calls that create a time-of-check window. Replaces the built-in list. Only the final dotted segment is compared, so `fs.existsSync` and `existsSync` are the same entry."}},additionalProperties:false}]},defaultOptions:[{ignoreInTests:true,fsMethods:DEFAULT_FS_METHODS}],create(context,[options={}]){const{ignoreInTests=true,fsMethods=DEFAULT_FS_METHODS}=options||{};const checkMethods=new Set(fsMethods.map(entry=>entry.split(".").at(-1)));const filename=context.filename;const isTestFile=ignoreInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);if(isTestFile){return{}}const sourceCode=context.sourceCode;function returnedExpression(callee){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type!=="FunctionName")return null;const body=def.node.body;if(body?.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return null;for(const statement of body.body){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&statement.argument){return statement.argument}}return null}function reachesPerUserRoot(node,depth=0){if(depth>8)return false;const next=child=>reachesPerUserRoot(child,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const variable=(0,provenance_1.findVariable)(sourceCode,node);const def=variable?.defs.length===1?variable.defs[0]:void 0;if(def?.type!=="Variable"||!def.node.init)return false;return next(def.node.init)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if(!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&PER_USER_ENV_VARS.has(node.property.name)){return true}return next(node.object)}case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;const name=callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property.name:null;if(name!==null&&PER_USER_ROOT_FUNCTIONS.has(name))return true;if(node.arguments.some(argument=>next(argument)))return true;const returned=returnedExpression(callee);return returned!==null&&next(returned)}case eslint_devkit_1.AST_NODE_TYPES.ConditionalExpression:return next(node.consequent)||next(node.alternate);case eslint_devkit_1.AST_NODE_TYPES.LogicalExpression:return next(node.left)||next(node.right);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>next(expression));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return next(node.left)||next(node.right);default:return false}}const EXISTENCE_CHECKS=new Set(["existsSync","exists"]);const READ_ONLY_USES=new Set(["readFileSync","readFile"]);function isSecurityRelevantWindow(checkMethod,useMethod){if(!EXISTENCE_CHECKS.has(checkMethod))return true;return!READ_ONLY_USES.has(useMethod)}function resolvesToNonFsLocal(callee,at){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const scope=sourceCode.getScope(at);if((0,eslint_devkit_1.isModuleBinding)(callee,scope,"fs")||(0,eslint_devkit_1.isModuleBinding)(callee,scope,"fs/promises")){return false}const variable=(0,provenance_1.findVariable)(sourceCode,callee);return!!variable&&variable.defs.length>0}function usedMethodName(node){const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return resolvesToNonFsLocal(callee,node)?"":callee.name}const name=memberName(callee);if(name===null)return"";const objectName=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.object.name:"";if(objectName==="fs"||objectName==="fsPromises")return name;const scope=sourceCode.getScope(node);return(0,eslint_devkit_1.isModuleBinding)(callee,scope,"fs")||(0,eslint_devkit_1.isModuleBinding)(callee,scope,"fs/promises")?name:""}function collectConditionCalls(node,out,depth=0){if(depth>8)return;const inner=(0,eslint_devkit_1.unwrapTypeSyntax)(node);switch(inner.type){case eslint_devkit_1.AST_NODE_TYPES.CallExpression:out.push(inner);return;case eslint_devkit_1.AST_NODE_TYPES.UnaryExpression:case eslint_devkit_1.AST_NODE_TYPES.AwaitExpression:collectConditionCalls(inner.argument,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.LogicalExpression:case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:collectConditionCalls(inner.left,out,depth+1);collectConditionCalls(inner.right,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.ConditionalExpression:collectConditionCalls(inner.test,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.ChainExpression:collectConditionCalls(inner.expression,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:collectConditionCalls(inner.object,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const init=(0,const_value_1.constInitializerOf)(sourceCode,inner);if(init!==null)collectConditionCalls(init,out,depth+1);return}default:return}}function sameTarget(checkArg,useArg){const check=(0,eslint_devkit_1.unwrapTypeSyntax)(checkArg);const use=(0,eslint_devkit_1.unwrapTypeSyntax)(useArg);if(check.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&use.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return check.name===use.name}return sourceCode.getText(check).replace(/\s/g,"")===sourceCode.getText(use).replace(/\s/g,"")}function hasRecursiveOption(node){return node.arguments.some(argument=>argument.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression&&argument.properties.some(property=>property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&!property.computed&&property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.key.name==="recursive"&&property.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value.value===true))}function checkCallExpression(node){const useMethodName=usedMethodName(node);if(!RISKY_USE_METHODS.has(useMethodName)){return}if(MKDIR_METHODS.has(useMethodName)&&hasRecursiveOption(node)){return}const rawUseArg=node.arguments[0];if(!rawUseArg)return;const useArg=(0,eslint_devkit_1.unwrapTypeSyntax)(rawUseArg);if(reachesPerUserRoot(useArg))return;let current=node.parent;while(current){if(current.type==="IfStatement"){const conditionCalls=[];collectConditionCalls(current.test,conditionCalls);for(const condition of conditionCalls){const checkMethodName=resolvesToNonFsLocal(condition.callee,condition)?"":memberName(condition.callee)??(condition.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?condition.callee.name:"");if(checkMethods.has(checkMethodName)&&isSecurityRelevantWindow(checkMethodName,useMethodName)){const checkArg=condition.arguments[0];if(checkArg&&sameTarget(checkArg,useArg)){reportToctou(node);return}}if(condition.callee.type==="MemberExpression"&&condition.callee.property.type==="Identifier"&&["isFile","isDirectory"].includes(condition.callee.property.name)&&condition.callee.object.type==="Identifier"){const statsVarName=condition.callee.object.name;let currentScope=sourceCode.getScope(condition);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===statsVarName)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init&&def.node.init.type==="CallExpression"){const init=def.node.init;const statMethod=memberName(init.callee)??(init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?init.callee.name:"");if(["statSync","lstatSync","stat","lstat"].includes(statMethod)){const statArg=init.arguments[0];if(statArg&&sameTarget(statArg,useArg)){reportToctou(node);return}}}}}}}current=current.parent}}function reportToctou(node){context.report({node,messageId:"toctouVulnerability"})}return{CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noToctouVulnerability=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const const_value_1=require("../../utils/const-value");const provenance_1=require("../../utils/provenance");const PER_USER_ROOT_FUNCTIONS=new Set(["homedir","userInfo"]);const PER_USER_ENV_VARS=new Set(["HOME","USERPROFILE","LOCALAPPDATA","APPDATA","XDG_CACHE_HOME","XDG_CONFIG_HOME","XDG_DATA_HOME","XDG_STATE_HOME"]);const DEFAULT_FS_METHODS=["fs.existsSync","fs.statSync","fs.accessSync","fs.exists","fs.stat","fs.access"];const MKDIR_METHODS=new Set(["mkdir","mkdirSync"]);const RISKY_USE_METHODS=new Set(["readFileSync","readFile","openSync","open","createReadStream","writeFileSync","writeFile","appendFileSync","appendFile","createWriteStream","unlinkSync","unlink","rmSync","rm","rmdirSync","rmdir","mkdirSync","mkdir","renameSync","rename","copyFileSync","copyFile","truncateSync","truncate","chmodSync","chmod","chownSync","chown","symlinkSync","symlink","linkSync","link"]);function memberName(callee){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;if(callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!callee.computed){return callee.property.name}return callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof callee.property.value==="string"?callee.property.value:null}exports.noToctouVulnerability=(0,eslint_devkit_2.createRule)({name:"no-toctou-vulnerability",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-toctou-vulnerability.md",description:"Detects Time-of-Check-Time-of-Use vulnerabilities",cwe:"CWE-367",cvss:7},messages:{toctouVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"TOCTOU vulnerability",cwe:"CWE-367",description:"Time-of-check Time-of-use race condition detected",severity:"HIGH",fix:"Act on the result and handle the failure \u2014 open/unlink and catch ENOENT \u2014 instead of checking first. Not a finding if the path is inside a directory only this user can write",documentationLink:"https://cwe.mitre.org/data/definitions/367.html"})},schema:[{type:"object",properties:{ignoreInTests:{type:"boolean",default:true},fsMethods:{type:"array",items:{type:"string"},default:DEFAULT_FS_METHODS,description:"Filesystem check calls that create a time-of-check window. Replaces the built-in list. Only the final dotted segment is compared, so `fs.existsSync` and `existsSync` are the same entry."}},additionalProperties:false}]},defaultOptions:[{ignoreInTests:true,fsMethods:DEFAULT_FS_METHODS}],create(context,[options={}]){const{ignoreInTests=true,fsMethods=DEFAULT_FS_METHODS}=options||{};const checkMethods=new Set(fsMethods.map(entry=>entry.split(".").at(-1)));const filename=context.filename;const isTestFile=ignoreInTests&&(0,eslint_devkit_1.isTestFilePath)(filename);if(isTestFile){return{}}const sourceCode=context.sourceCode;function returnedExpression(callee){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type!=="FunctionName")return null;const body=def.node.body;if(body?.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return null;for(const statement of body.body){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&statement.argument){return statement.argument}}return null}function reachesPerUserRoot(node,depth=0){if(depth>8)return false;const next=child=>reachesPerUserRoot(child,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const variable=(0,provenance_1.findVariable)(sourceCode,node);const def=variable?.defs.length===1?variable.defs[0]:void 0;if(def?.type!=="Variable"||!def.node.init)return false;return next(def.node.init)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if((0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(node),PER_USER_ENV_VARS)){return true}return next(node.object)}case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;const name=callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression?(0,eslint_devkit_1.propertyName)(callee):null;if(name!==null&&PER_USER_ROOT_FUNCTIONS.has(name))return true;if(node.arguments.some(argument=>next(argument)))return true;const returned=returnedExpression(callee);return returned!==null&&next(returned)}case eslint_devkit_1.AST_NODE_TYPES.ConditionalExpression:return next(node.consequent)||next(node.alternate);case eslint_devkit_1.AST_NODE_TYPES.LogicalExpression:return next(node.left)||next(node.right);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>next(expression));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return next(node.left)||next(node.right);default:return false}}const EXISTENCE_CHECKS=new Set(["existsSync","exists"]);const READ_ONLY_USES=new Set(["readFileSync","readFile"]);function isSecurityRelevantWindow(checkMethod,useMethod){if(!EXISTENCE_CHECKS.has(checkMethod))return true;return!READ_ONLY_USES.has(useMethod)}function resolvesToNonFsLocal(callee,at){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const scope=sourceCode.getScope(at);if((0,eslint_devkit_1.isModuleBinding)(callee,scope,"fs")||(0,eslint_devkit_1.isModuleBinding)(callee,scope,"fs/promises")){return false}const variable=(0,provenance_1.findVariable)(sourceCode,callee);return!!variable&&variable.defs.length>0}function usedMethodName(node){const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return resolvesToNonFsLocal(callee,node)?"":callee.name}const name=memberName(callee);if(name===null)return"";const objectName=callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.object.name:"";if(objectName==="fs"||objectName==="fsPromises")return name;const scope=sourceCode.getScope(node);return(0,eslint_devkit_1.isModuleBinding)(callee,scope,"fs")||(0,eslint_devkit_1.isModuleBinding)(callee,scope,"fs/promises")?name:""}function collectConditionCalls(node,out,depth=0){if(depth>8)return;const inner=(0,eslint_devkit_1.unwrapTypeSyntax)(node);switch(inner.type){case eslint_devkit_1.AST_NODE_TYPES.CallExpression:out.push(inner);return;case eslint_devkit_1.AST_NODE_TYPES.UnaryExpression:case eslint_devkit_1.AST_NODE_TYPES.AwaitExpression:collectConditionCalls(inner.argument,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.LogicalExpression:case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:collectConditionCalls(inner.left,out,depth+1);collectConditionCalls(inner.right,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.ConditionalExpression:collectConditionCalls(inner.test,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.ChainExpression:collectConditionCalls(inner.expression,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:collectConditionCalls(inner.object,out,depth+1);return;case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const init=(0,const_value_1.constInitializerOf)(sourceCode,inner);if(init!==null)collectConditionCalls(init,out,depth+1);return}default:return}}function sameTarget(checkArg,useArg){const check=(0,eslint_devkit_1.unwrapTypeSyntax)(checkArg);const use=(0,eslint_devkit_1.unwrapTypeSyntax)(useArg);if(check.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&use.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return check.name===use.name}return sourceCode.getText(check).replace(/\s/g,"")===sourceCode.getText(use).replace(/\s/g,"")}function hasRecursiveOption(node){return node.arguments.some(argument=>argument.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression&&argument.properties.some(property=>property.type===eslint_devkit_1.AST_NODE_TYPES.Property&&!property.computed&&property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.key.name==="recursive"&&property.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&property.value.value===true))}function checkCallExpression(node){const useMethodName=usedMethodName(node);if(!RISKY_USE_METHODS.has(useMethodName)){return}if(MKDIR_METHODS.has(useMethodName)&&hasRecursiveOption(node)){return}const rawUseArg=node.arguments[0];if(!rawUseArg)return;const useArg=(0,eslint_devkit_1.unwrapTypeSyntax)(rawUseArg);if(reachesPerUserRoot(useArg))return;let current=node.parent;while(current){if(current.type==="IfStatement"){const conditionCalls=[];collectConditionCalls(current.test,conditionCalls);for(const condition of conditionCalls){const checkMethodName=resolvesToNonFsLocal(condition.callee,condition)?"":memberName(condition.callee)??(condition.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?condition.callee.name:"");if(checkMethods.has(checkMethodName)&&isSecurityRelevantWindow(checkMethodName,useMethodName)){const checkArg=condition.arguments[0];if(checkArg&&sameTarget(checkArg,useArg)){reportToctou(node);return}}if(condition.callee.type==="MemberExpression"&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(condition.callee),["isFile","isDirectory"])&&condition.callee.object.type==="Identifier"){const statsVarName=condition.callee.object.name;let currentScope=sourceCode.getScope(condition);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===statsVarName)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init&&def.node.init.type==="CallExpression"){const init=def.node.init;const statMethod=memberName(init.callee)??(init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?init.callee.name:"");if(["statSync","lstatSync","stat","lstat"].includes(statMethod)){const statArg=init.arguments[0];if(statArg&&sameTarget(statArg,useArg)){reportToctou(node);return}}}}}}}current=current.parent}}function reportToctou(node){context.report({node,messageId:"toctouVulnerability"})}return{CallExpression:checkCallExpression}}});