eslint-plugin-node-security 4.10.0 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +29 -23
  2. package/package.json +3 -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/detect-suspicious-dependencies/index.js +1 -1
  8. package/src/rules/lock-file/index.js +1 -1
  9. package/src/rules/no-arbitrary-file-access/index.js +1 -1
  10. package/src/rules/no-buffer-overread/index.js +1 -1
  11. package/src/rules/no-data-in-temp-storage/index.js +1 -1
  12. package/src/rules/no-dynamic-algorithm-selection/index.js +1 -1
  13. package/src/rules/no-env-injection/index.js +1 -0
  14. package/src/rules/no-insecure-http-parser/index.js +1 -0
  15. package/src/rules/no-math-random-crypto/index.js +1 -1
  16. package/src/rules/no-shell-injection/index.js +1 -1
  17. package/src/rules/no-ssrf/index.js +1 -1
  18. package/src/rules/no-timing-unsafe-compare/index.js +1 -1
  19. package/src/rules/no-toctou-vulnerability/index.js +1 -1
  20. package/src/rules/no-unbounded-decompression/index.js +1 -0
  21. package/src/rules/no-unsafe-buffer-alloc/index.js +1 -1
  22. package/src/rules/no-unsafe-dynamic-require/index.js +1 -1
  23. package/src/rules/no-weak-hash-algorithm/index.js +1 -1
  24. package/src/rules/no-zip-slip/index.js +1 -1
  25. package/src/rules/require-aead-tag-verification/index.js +1 -0
  26. package/src/rules/require-dependency-integrity/index.js +1 -1
  27. package/src/rules/require-stream-error-handler/index.js +1 -0
  28. package/src/utils/constant-folding.js +1 -0
  29. package/src/utils/names.js +1 -0
  30. package/src/utils/provenance.js +1 -0
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noBufferOverread=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noBufferOverread=(0,eslint_devkit_1.createRule)({name:"no-buffer-overread",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-buffer-overread.md",description:"Detects buffer access beyond bounds",cwe:"CWE-126"},messages:{bufferOverread:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer Overread",cwe:"CWE-126",description:"Buffer access beyond allocated bounds",severity:"{{severity}}",fix:"{{safeAlternative}}",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Access",cwe:"CWE-126",description:"Buffer accessed without bounds validation",severity:"HIGH",fix:"Add bounds check before buffer access",documentationLink:"https://nodejs.org/api/buffer.html"}),missingBoundsCheck:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Bounds Check",cwe:"CWE-126",description:"Buffer operation missing bounds validation",severity:"MEDIUM",fix:"Validate indices before buffer operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),negativeBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Negative Buffer Index",cwe:"CWE-126",description:"Negative index used for buffer access",severity:"MEDIUM",fix:"Ensure buffer indices are non-negative",documentationLink:"https://nodejs.org/api/buffer.html"}),userControlledBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"User Controlled Buffer Index",cwe:"CWE-126",description:"Buffer accessed with user-controlled index",severity:"HIGH",fix:"Validate user input before using as buffer index",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferSlice:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Slice",cwe:"CWE-126",description:"Buffer slice with unvalidated indices",severity:"MEDIUM",fix:"Validate slice start/end indices",documentationLink:"https://nodejs.org/api/buffer.html#bufslicestart-end"}),bufferLengthNotChecked:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer Length Not Checked",cwe:"CWE-126",description:"Buffer length not validated before access",severity:"MEDIUM",fix:"Check buffer.length before operations",documentationLink:"https://nodejs.org/api/buffer.html#buflength"}),useSafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safe Buffer Access",description:"Use bounds-checked buffer access methods",severity:"LOW",fix:"Use buffer.read*() with offset validation or safe wrapper functions",documentationLink:"https://nodejs.org/api/buffer.html"}),validateBufferIndices:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Buffer Indices",description:"Validate buffer indices before use",severity:"LOW",fix:"Check 0 <= index < buffer.length",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),checkBufferBounds:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Check Buffer Bounds",description:"Always check buffer bounds",severity:"LOW",fix:"Validate buffer operations against buffer.length",documentationLink:"https://nodejs.org/api/buffer.html#buflength"}),strategyBoundsChecking:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Bounds Checking Strategy",description:"Implement comprehensive bounds checking",severity:"LOW",fix:"Validate all buffer indices and lengths before operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),strategyInputValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Input Validation Strategy",description:"Validate user input used as buffer indices",severity:"LOW",fix:"Sanitize and validate all user input before buffer operations",documentationLink:"https://nodejs.org/api/buffer.html"}),strategySafeBuffers:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Safe Buffer Strategy",description:"Use safe buffer wrapper libraries",severity:"LOW",fix:"Use libraries that provide bounds-checked buffer operations",documentationLink:"https://www.npmjs.com/package/safe-buffer"})},schema:[{type:"object",properties:{bufferMethods:{type:"array",items:{type:"string"},default:["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],description:"Buffer read/write methods checked for bounds"},boundsCheckFunctions:{type:"array",items:{type:"string"},default:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],description:"Function names that count as a bounds check"},bufferTypes:{type:"array",items:{type:"string"},default:["Buffer","Uint8Array","ArrayBuffer","DataView"],description:"Constructor names treated as buffer types"},trustedSanitizers:{type:"array",items:{type:"string"},default:[],description:"Additional function names to consider as buffer index validators"},trustedAnnotations:{type:"array",items:{type:"string"},default:[],description:"Additional JSDoc annotations to consider as safe markers"},strictMode:{type:"boolean",default:false,description:"Disable all false positive detection (strict mode)"}},additionalProperties:false}]},defaultOptions:[{bufferMethods:["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],boundsCheckFunctions:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes:["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const options=context.options[0]||{};const{bufferMethods=["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","copy"],boundsCheckFunctions=["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes=["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers=[],trustedAnnotations=[],strictMode=false}=options;const sourceCode=context.sourceCode;const filename=context.filename;const safetyChecker=(0,eslint_devkit_1.createSafetyChecker)({trustedSanitizers,trustedAnnotations,trustedOrmPatterns:[],strictMode});const bufferTypesSet=new Set(bufferTypes.map(t=>t.toLowerCase()));const userControlledKeywords=new Set(["req","request","query","params","input","user","offset","index","body"]);const bufferVars=new Set;const isBufferType=varName=>{if(bufferVars.has(varName))return true;const lowerName=varName.toLowerCase();for(const type of bufferTypesSet){if(lowerName.includes(type))return true}if(lowerName==="buf"||lowerName==="bytes")return true;return false};const isUserControlledIndex=indexNode=>{if(indexNode.type==="MemberExpression"){let walker=indexNode;while(walker.type==="MemberExpression"){walker=walker.object}if(walker.type==="Identifier"){const root=walker.name.toLowerCase();if(["req","request","event","ctx","context"].includes(root)){return true}for(const keyword of userControlledKeywords){if(root.includes(keyword))return true}}}if(indexNode.type==="Identifier"){const varName=indexNode.name.toLowerCase();for(const keyword of userControlledKeywords){if(varName.includes(keyword))return true}let currentScope=sourceCode.getScope(indexNode);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===indexNode.name)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init){const init=def.node.init;if(init.type==="MemberExpression"){const objectText=sourceCode.getText(init.object).toLowerCase();const propertyText=sourceCode.getText(init.property).toLowerCase();const keywords=["req","request","query","params","input","user","body"];if(keywords.some(k=>objectText.includes(k)||propertyText.includes(k))){return true}}if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const typeConversionFunctions=["number","parseint","parsefloat","string","boolean"];let isTypeConversion=false;if(init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){isTypeConversion=typeConversionFunctions.includes(init.callee.name.toLowerCase())}if(isTypeConversion&&init.arguments.length>0){return isUserControlledIndex(init.arguments[0])}}if(init.type==="Identifier"&&init.name!==indexNode.name){return isUserControlledIndex(init)}}}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const typeConversionFunctions=["Number","parseInt","parseFloat","String","Boolean"];if(indexNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&typeConversionFunctions.includes(indexNode.callee.name)){for(const arg of indexNode.arguments){if(isUserControlledIndex(arg)){return true}}}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const text=sourceCode.getText(indexNode).toLowerCase();const keywords=["req.","request.","query.","params.","body.","input.","user."];if(keywords.some(k=>text.includes(k))){return true}}return false};const isIndexValidated=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.value==="number"){return indexNode.value>=0}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&current.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&current.id.name===indexNode.name&&current.init){const init=current.init;if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&boundsCheckFunctions.includes(init.callee.name)){return true}if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&init.callee.object.name==="Math"&&init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(init.callee.property.name==="min"||init.callee.property.name==="max")){return true}break}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){const params=current.params;for(const param of params){if(param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&param.name===indexNode.name){return true}}}current=current.parent}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&indexNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&boundsCheckFunctions.includes(indexNode.callee.name)){return true}return false};const hasBoundsCheck=(bufferName,indexNode)=>{let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){break}if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){const condition=current.test;const conditionText=sourceCode.getText(condition).toLowerCase();if(conditionText.includes(`${bufferName}.length`)&&(conditionText.includes("<")||conditionText.includes("<=")||conditionText.includes(">")||conditionText.includes(">=")||conditionText.includes("&&")||conditionText.includes("||"))){return true}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration){for(const declarator of current.declarations){if(declarator.init){const initText=sourceCode.getText(declarator.init).toLowerCase();if(initText.includes(`${bufferName}.length`)&&(initText.includes("math.min")||initText.includes("math.max")||initText.includes("mathmin")||initText.includes("mathmax"))){return true}}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&current.argument){const returnText=sourceCode.getText(current.argument).toLowerCase();if(returnText.includes(`${bufferName}.length`)){return true}}current=current.parent}return false};const couldBeNegative=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.value==="number"){return indexNode.value<0}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&indexNode.operator==="-"&&indexNode.argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.argument.value==="number"){return true}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&indexNode.operator==="-"){return true}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&current.init){if(current.init.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof current.init.value==="number"&&current.init.value<0){return true}if(current.init.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&current.init.operator==="-"&&current.init.argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof current.init.argument.value==="number"){return true}}current=current.parent}}return false};return{VariableDeclarator(node){if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init){const varName=node.id.name;if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferTypes.includes(node.init.callee.name)){bufferVars.add(varName)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init.callee.object.name==="Buffer"&&node.init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["from","alloc","allocUnsafe"].includes(node.init.callee.property.name)){bufferVars.add(varName)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.init.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)){bufferVars.add(varName)}}if(bufferTypes.some(type=>varName.toLowerCase().includes(type.toLowerCase()))){bufferVars.add(varName)}}},MemberExpression(node){if(node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const bufferName=node.object.name;const indexNode=node.property;if(isBufferType(bufferName)){if(couldBeNegative(indexNode)){context.report({node,messageId:"negativeBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}if(isUserControlledIndex(indexNode)&&!isIndexValidated(indexNode)){if(!hasBoundsCheck(bufferName,indexNode)){if(safetyChecker.isSafe(node,context)){return}context.report({node,messageId:"userControlledBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}}if(!hasBoundsCheck(bufferName,indexNode)&&!isIndexValidated(indexNode)){if(safetyChecker.isSafe(node,context)){return}context.report({node,messageId:"unsafeBufferAccess",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(node.property.type==="Identifier"&&bufferMethods.includes(node.property.name)&&node.object.type==="Identifier"&&isBufferType(node.object.name)){}},CallExpression(node){const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&callee.property.name==="slice"&&callee.object.type==="Identifier"&&isBufferType(callee.object.name)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)){continue}context.report({node:arg,messageId:"unsafeBufferSlice",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object.name)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)){continue}context.report({node:arg,messageId:"missingBoundsCheck",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}},BinaryExpression(node){const leftText=sourceCode.getText(node.left);const rightText=sourceCode.getText(node.right);if(leftText.includes(".length")||rightText.includes(".length")){}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noBufferOverread=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const VIEW_METHODS=new Set(["slice","subarray"]);exports.noBufferOverread=(0,eslint_devkit_1.createRule)({name:"no-buffer-overread",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-buffer-overread.md",description:"Detects buffer access beyond bounds",cwe:"CWE-126"},messages:{bufferOverread:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer Overread",cwe:"CWE-126",description:"Buffer access beyond allocated bounds",severity:"{{severity}}",fix:"{{safeAlternative}}",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Access",cwe:"CWE-126",description:"Buffer accessed without bounds validation",severity:"HIGH",fix:"Add bounds check before buffer access",documentationLink:"https://nodejs.org/api/buffer.html"}),missingBoundsCheck:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Bounds Check",cwe:"CWE-126",description:"Buffer operation missing bounds validation",severity:"MEDIUM",fix:"Validate indices before buffer operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),negativeBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Negative Buffer Index",cwe:"CWE-126",description:"Negative index used for buffer access",severity:"MEDIUM",fix:"Ensure buffer indices are non-negative",documentationLink:"https://nodejs.org/api/buffer.html"}),userControlledBufferIndex:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"User Controlled Buffer Index",cwe:"CWE-126",description:"Buffer accessed with user-controlled index",severity:"HIGH",fix:"Validate user input before using as buffer index",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),unsafeBufferSlice:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Buffer Slice",cwe:"CWE-126",description:"Buffer slice with unvalidated indices",severity:"MEDIUM",fix:"Validate slice start/end indices",documentationLink:"https://nodejs.org/api/buffer.html#bufslicestart-end"}),bufferLengthNotChecked:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Buffer Length Not Checked",cwe:"CWE-126",description:"Buffer length not validated before access",severity:"MEDIUM",fix:"Check buffer.length before operations",documentationLink:"https://nodejs.org/api/buffer.html#buflength"}),useSafeBufferAccess:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safe Buffer Access",description:"Use bounds-checked buffer access methods",severity:"LOW",fix:"Use buffer.read*() with offset validation or safe wrapper functions",documentationLink:"https://nodejs.org/api/buffer.html"}),validateBufferIndices:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Buffer Indices",description:"Validate buffer indices before use",severity:"LOW",fix:"Check 0 <= index < buffer.length",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),checkBufferBounds:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Check Buffer Bounds",description:"Always check buffer bounds",severity:"LOW",fix:"Validate buffer operations against buffer.length",documentationLink:"https://nodejs.org/api/buffer.html#buflength"}),strategyBoundsChecking:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Bounds Checking Strategy",description:"Implement comprehensive bounds checking",severity:"LOW",fix:"Validate all buffer indices and lengths before operations",documentationLink:"https://cwe.mitre.org/data/definitions/126.html"}),strategyInputValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Input Validation Strategy",description:"Validate user input used as buffer indices",severity:"LOW",fix:"Sanitize and validate all user input before buffer operations",documentationLink:"https://nodejs.org/api/buffer.html"}),strategySafeBuffers:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Safe Buffer Strategy",description:"Use safe buffer wrapper libraries",severity:"LOW",fix:"Use libraries that provide bounds-checked buffer operations",documentationLink:"https://www.npmjs.com/package/safe-buffer"})},schema:[{type:"object",properties:{bufferMethods:{type:"array",items:{type:"string"},default:["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","subarray","copy"],description:"Buffer read/write methods checked for bounds"},boundsCheckFunctions:{type:"array",items:{type:"string"},default:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],description:"Function names that count as a bounds check"},bufferTypes:{type:"array",items:{type:"string"},default:["Buffer","Uint8Array","ArrayBuffer","DataView"],description:"Constructor names treated as buffer types"},trustedSanitizers:{type:"array",items:{type:"string"},default:[],description:"Additional function names to consider as buffer index validators"},trustedAnnotations:{type:"array",items:{type:"string"},default:[],description:"Additional JSDoc annotations to consider as safe markers"},strictMode:{type:"boolean",default:false,description:"Disable all false positive detection (strict mode)"},reportUnvalidatedIndices:{type:"boolean",default:false,description:"Report every index that cannot be proven validated. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{bufferMethods:["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","subarray","copy"],boundsCheckFunctions:["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes:["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const options=context.options[0]||{};const{bufferMethods=["readUInt8","readUInt16LE","readUInt32LE","readInt8","readInt16LE","readInt32LE","writeUInt8","writeUInt16LE","writeUInt32LE","slice","subarray","copy"],boundsCheckFunctions=["validateIndex","checkBounds","safeIndex","validateBufferIndex"],bufferTypes=["Buffer","Uint8Array","ArrayBuffer","DataView"],trustedSanitizers=[],trustedAnnotations=[],strictMode=false,reportUnvalidatedIndices=false}=options;const sourceCode=context.sourceCode;const filename=context.filename;const safetyChecker=(0,eslint_devkit_1.createSafetyChecker)({trustedSanitizers,trustedAnnotations,trustedOrmPatterns:[],strictMode});const bufferTypesSet=new Set(bufferTypes.map(t=>t.toLowerCase()));const userControlledKeywords=new Set(["req","request","query","params","input","user","offset","index","body"]);const bufferVars=new Set;const addBufferVar=id=>{const variable=(0,provenance_1.findVariable)(sourceCode,id);if(variable)bufferVars.add(variable)};const isBufferType=node=>{const variable=(0,provenance_1.findVariable)(sourceCode,node);if(variable&&bufferVars.has(variable))return true;const lowerName=node.name.toLowerCase();for(const type of bufferTypesSet){if(lowerName.includes(type))return true}if(lowerName==="buf"||lowerName==="bytes")return true;return false};const isWriteTarget=node=>{const parent=node.parent;if(!parent)return false;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression)return parent.left===node;return parent.type===eslint_devkit_1.AST_NODE_TYPES.UpdateExpression};const isLoopBounded=indexNode=>{if(indexNode.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const name=indexNode.name;let current=indexNode.parent;while(current){const test=current.type===eslint_devkit_1.AST_NODE_TYPES.ForStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.WhileStatement?current.test:null;if(test&&test.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&(test.operator==="<"||test.operator==="<=")&&test.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&test.left.name===name){return true}current=current.parent}return false};const isUserControlledIndex=indexNode=>{if(indexNode.type==="MemberExpression"){let walker=indexNode;while(walker.type==="MemberExpression"){walker=walker.object}if(walker.type==="Identifier"){const root=walker.name.toLowerCase();if(["req","request","event","ctx","context"].includes(root)){return true}for(const keyword of userControlledKeywords){if(root.includes(keyword))return true}}}if(indexNode.type==="Identifier"){const varName=indexNode.name.toLowerCase();for(const keyword of userControlledKeywords){if(varName.includes(keyword))return true}let currentScope=sourceCode.getScope(indexNode);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===indexNode.name)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init){const init=def.node.init;if(init.type==="MemberExpression"){const objectText=sourceCode.getText(init.object).toLowerCase();const propertyText=sourceCode.getText(init.property).toLowerCase();const keywords=["req","request","query","params","input","user","body"];if(keywords.some(k=>objectText.includes(k)||propertyText.includes(k))){return true}}if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const typeConversionFunctions=["number","parseint","parsefloat","string","boolean"];let isTypeConversion=false;if(init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){isTypeConversion=typeConversionFunctions.includes(init.callee.name.toLowerCase())}if(isTypeConversion&&init.arguments.length>0){return isUserControlledIndex(init.arguments[0])}}if(init.type==="Identifier"&&init.name!==indexNode.name){return isUserControlledIndex(init)}}}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const typeConversionFunctions=["Number","parseInt","parseFloat","String","Boolean"];if(indexNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&typeConversionFunctions.includes(indexNode.callee.name)){for(const arg of indexNode.arguments){if(isUserControlledIndex(arg)){return true}}}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const text=sourceCode.getText(indexNode).toLowerCase();const keywords=["req.","request.","query.","params.","body.","input.","user."];if(keywords.some(k=>text.includes(k))){return true}}return false};const isIndexValidated=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.value==="number"){return indexNode.value>=0}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&current.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&current.id.name===indexNode.name&&current.init){const init=current.init;if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&boundsCheckFunctions.includes(init.callee.name)){return true}if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&init.callee.object.name==="Math"&&init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(init.callee.property.name==="min"||init.callee.property.name==="max")){return true}break}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){const params=current.params;for(const param of params){if(param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&param.name===indexNode.name){return true}}}current=current.parent}}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&indexNode.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&boundsCheckFunctions.includes(indexNode.callee.name)){return true}return false};const hasBoundsCheck=(bufferName,indexNode)=>{let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){break}if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){const condition=current.test;const conditionText=sourceCode.getText(condition).toLowerCase();if(conditionText.includes(`${bufferName}.length`)&&(conditionText.includes("<")||conditionText.includes("<=")||conditionText.includes(">")||conditionText.includes(">=")||conditionText.includes("&&")||conditionText.includes("||"))){return true}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration){for(const declarator of current.declarations){if(declarator.init){const initText=sourceCode.getText(declarator.init).toLowerCase();if(initText.includes(`${bufferName}.length`)&&(initText.includes("math.min")||initText.includes("math.max")||initText.includes("mathmin")||initText.includes("mathmax"))){return true}}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&current.argument){const returnText=sourceCode.getText(current.argument).toLowerCase();if(returnText.includes(`${bufferName}.length`)){return true}}current=current.parent}return false};const couldBeNegative=indexNode=>{if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.value==="number"){return indexNode.value<0}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&indexNode.operator==="-"&&indexNode.argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof indexNode.argument.value==="number"){return true}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression&&indexNode.operator==="-"){return true}if(indexNode.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){let current=indexNode;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&current.init){if(current.init.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof current.init.value==="number"&&current.init.value<0){return true}if(current.init.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&current.init.operator==="-"&&current.init.argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof current.init.argument.value==="number"){return true}}current=current.parent}}return false};return{VariableDeclarator(node){if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init){const varName=node.id.name;if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferTypes.includes(node.init.callee.name)){addBufferVar(node.id)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.init.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.init.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.init.callee.object.name==="Buffer"&&node.init.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&["from","alloc","allocUnsafe"].includes(node.init.callee.property.name)){addBufferVar(node.id)}if(node.init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=node.init.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object)){addBufferVar(node.id)}}if(bufferTypes.some(type=>varName.toLowerCase().includes(type.toLowerCase()))){addBufferVar(node.id)}}},MemberExpression(node){if(node.computed&&node.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const bufferName=node.object.name;const indexNode=node.property;if(isWriteTarget(node))return;if(isLoopBounded(indexNode))return;if(isBufferType(node.object)){if(couldBeNegative(indexNode)){context.report({node,messageId:"negativeBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}if(isUserControlledIndex(indexNode)&&!isIndexValidated(indexNode)){if(!hasBoundsCheck(bufferName,indexNode)){if(safetyChecker.isSafe(node,context)){return}context.report({node,messageId:"userControlledBufferIndex",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}}if(reportUnvalidatedIndices&&!hasBoundsCheck(bufferName,indexNode)&&!isIndexValidated(indexNode)){if(safetyChecker.isSafe(node,context)){return}context.report({node,messageId:"unsafeBufferAccess",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(node.property.type==="Identifier"&&bufferMethods.includes(node.property.name)&&node.object.type==="Identifier"&&isBufferType(node.object)){}},CallExpression(node){const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&VIEW_METHODS.has(callee.property.name)&&callee.object.type==="Identifier"&&isBufferType(callee.object)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)){continue}context.report({node:arg,messageId:"unsafeBufferSlice",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&bufferMethods.includes(callee.property.name)&&!VIEW_METHODS.has(callee.property.name)&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&isBufferType(callee.object)){const args=node.arguments;for(const arg of args){if(isUserControlledIndex(arg)&&!isIndexValidated(arg)){if(safetyChecker.isSafe(node,context)){continue}context.report({node:arg,messageId:"missingBoundsCheck",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}},BinaryExpression(node){const leftText=sourceCode.getText(node.left);const rightText=sourceCode.getText(node.right);if(leftText.includes(".length")||rightText.includes(".length")){}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDataInTempStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEFAULT_TEMP_PATHS=["/tmp","/var/tmp","temp/","/temp"];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:7.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"})},schema:[{type:"object",properties:{tempPaths:{type:"array",items:{type:"string"},description:"Custom list of temporary paths to flag"},ignoreFiles:{type:"array",items:{type:"string"},description:"List of files or patterns to ignore"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const options=context.options[0]||{};const tempPaths=options.tempPaths||DEFAULT_TEMP_PATHS;const ignoreFiles=options.ignoreFiles||[];const filename=context.filename;if(ignoreFiles.some(pattern=>filename.includes(pattern))){return{}}function report(node){context.report({node,messageId:"violationDetected"})}return{CallExpression(node){if(node.callee.type==="MemberExpression"&&node.callee.object.type==="Identifier"&&node.callee.object.name==="fs"&&node.callee.property.type==="Identifier"&&["writeFileSync","writeFile"].includes(node.callee.property.name)){const pathArg=node.arguments[0];if(pathArg&&pathArg.type==="Literal"&&typeof pathArg.value==="string"){if(tempPaths.some(tp=>pathArg.value.includes(tp))){report(pathArg)}}}},Literal(node){if(typeof node.value==="string"){if(tempPaths.some(tp=>node.value.includes(tp))){const parent=node.parent;if(parent?.type==="VariableDeclarator"||parent?.type==="AssignmentExpression"){report(node)}}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDataInTempStorage=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TEMP_PATHS=["/tmp","/var/tmp","temp/","/temp"];const FS_WRITE_FUNCTIONS=["writeFileSync","writeFile"];function containsPathSegments(haystack,needle){const split=value=>value.split(/[/\\]/).filter(segment=>segment.length>0);const target=split(needle);if(target.length===0)return false;const segments=split(haystack);for(let start=0;start+target.length<=segments.length;start+=1){if(target.every((segment,offset)=>segments[start+offset]===segment))return true}return false}function isTmpdirCall(node){return node.type==="CallExpression"&&node.callee.type==="MemberExpression"&&!node.callee.computed&&node.callee.object.type==="Identifier"&&node.callee.object.name==="os"&&node.callee.property.type==="Identifier"&&node.callee.property.name==="tmpdir"}function isStaticTmpdirJoin(node){const{callee}=node;if(callee.type!=="MemberExpression"||callee.computed)return false;if(callee.property.type!=="Identifier")return false;if(callee.property.name!=="join"&&callee.property.name!=="resolve"){return false}if(callee.object.type!=="Identifier"||callee.object.name!=="path"){return false}if(node.arguments.length<2)return false;if(!isTmpdirCall(node.arguments[0]))return false;return node.arguments.slice(1).every(arg=>arg.type==="Literal"&&typeof arg.value==="string")}exports.noDataInTempStorage=(0,eslint_devkit_1.createRule)({name:"no-data-in-temp-storage",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-data-in-temp-storage.md",description:"Prevent sensitive data in temp directories",cwe:"CWE-312",cvss:7.5},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Temp Storage Data",cwe:"CWE-312",description:"Sensitive data written to temp directory - not secure",severity:"HIGH",fix:"Use secure storage location or encrypt data before writing",documentationLink:"https://cwe.mitre.org/data/definitions/312.html"}),predictableTempPath:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Predictable Temp File Name (CWE-377)",cwe:"CWE-377",cvss:7.5,description:"path.join(os.tmpdir(), \u2026) with a constant name resolves to the same path on every run, in a directory every local user can write. An attacker who pre-creates that name \u2014 or symlinks it at a file they want clobbered \u2014 wins the race before the write happens.",severity:"HIGH",fix:"Create the file inside a fresh directory from fs.mkdtemp/mkdtempSync, or add a crypto.randomUUID() segment to the name.",documentationLink:"https://cwe.mitre.org/data/definitions/377.html"})},schema:[{type:"object",properties:{tempPaths:{type:"array",items:{type:"string"},description:"Custom list of temporary paths to flag"},ignoreFiles:{type:"array",items:{type:"string"},description:"List of files or patterns to ignore"}},additionalProperties:false}]},defaultOptions:[{}],create(context){const options=context.options[0]||{};const tempPaths=options.tempPaths||DEFAULT_TEMP_PATHS;const ignoreFiles=options.ignoreFiles||[];const filename=context.filename;if(ignoreFiles.some(pattern=>filename.includes(pattern))){return{}}function report(node){context.report({node,messageId:"violationDetected"})}function reportIfPredictable(candidate){if(!candidate||candidate.type!=="CallExpression")return;if(!isStaticTmpdirJoin(candidate))return;context.report({node:candidate,messageId:"predictableTempPath"})}function isFsWriteCall(node){return node?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.callee.computed&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.object.name==="fs"&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&FS_WRITE_FUNCTIONS.includes(node.callee.property.name)}function isWrittenThrough(identifier){const variable=(0,provenance_1.findVariable)(context.sourceCode,identifier);if(!variable)return false;return variable.references.some(reference=>{const parent=reference.identifier.parent;return isFsWriteCall(parent)&&parent.arguments[0]===reference.identifier})}function boundName(parent){if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return parent.id}if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&parent.left.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return parent.left}return null}return{CallExpression(node){if(!isFsWriteCall(node))return;const pathArg=node.arguments[0];if(pathArg?.type==="Literal"&&typeof pathArg.value==="string"){const value=pathArg.value;if(tempPaths.some(tp=>containsPathSegments(value,tp))){report(pathArg)}}reportIfPredictable(pathArg)},VariableDeclarator(node){reportIfPredictable(node.init)},AssignmentExpression(node){reportIfPredictable(node.right)},Literal(node){const value=node.value;if(typeof value!=="string")return;if(!tempPaths.some(tp=>containsPathSegments(value,tp)))return;const name=boundName(node.parent);if(name!==null&&isWrittenThrough(name))report(node)}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicAlgorithmSelection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");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){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(firstArg.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof firstArg.value==="string")return;if(firstArg.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral&&firstArg.expressions.length===0)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;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}})}}}});
@@ -0,0 +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:{extraRequestRoots:{type:"array",items:{type:"string"},description:"Extra identifiers to treat as roots of request-controlled data"}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options]){const{extraRequestRoots}=options;const requestRoots=new Set([...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}}}}}});
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noInsecureHttpParser=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noInsecureHttpParser=(0,eslint_devkit_1.createRule)({name:"no-insecure-http-parser",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-insecure-http-parser.md",description:"Disallow insecureHTTPParser: true on Node HTTP servers and clients",cwe:"CWE-444",cvss:7.5},hasSuggestions:true,messages:{insecureHttpParser:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Lenient HTTP parser enabled",cwe:"CWE-444",description:"insecureHTTPParser: true accepts ambiguous message framing (conflicting Content-Length/Transfer-Encoding, invalid chunk sizes). When a front-end proxy and this process disagree on request boundaries, an attacker can smuggle a request onto another user connection.",severity:"HIGH",fix:"Remove insecureHTTPParser (or set it to false) and fix the upstream that emits malformed framing",documentationLink:"https://portswigger.net/web-security/request-smuggling"}),useStrictParser:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use the strict parser",description:"Node's default llhttp parser rejects ambiguous framing",severity:"LOW",fix:"insecureHTTPParser: false (or remove the option)",documentationLink:"https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow the lenient parser in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(context.filename);function isParserKey(key,computed){if(key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return!computed&&key.name==="insecureHTTPParser"}return key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&key.value==="insecureHTTPParser"}function enablesLenientParser(value){return value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&value.value===true}function report(node,value){context.report({node,messageId:"insecureHttpParser",suggest:[{messageId:"useStrictParser",fix:fixer=>fixer.replaceText(value,"false")}]})}return{Property(node){if(isTestFile)return;if(!isParserKey(node.key,node.computed))return;if(!enablesLenientParser(node.value))return;report(node,node.value)},AssignmentExpression(node){if(isTestFile)return;if(node.left.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return;if(!isParserKey(node.left.property,node.left.computed))return;if(!enablesLenientParser(node.right))return;report(node,node.right)}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noMathRandomCrypto=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const CRYPTO_VARIABLE_PATTERNS=[/token/i,/key/i,/secret/i,/password/i,/salt/i,/iv/i,/nonce/i,/random/i,/seed/i,/hash/i,/cipher/i,/encrypt/i,/auth/i,/session/i,/csrf/i,/otp/i,/pin/i,/code/i,/verify/i];const CRYPTO_FUNCTION_PATTERNS=[/generate.*token/i,/generate.*key/i,/generate.*id/i,/create.*secret/i,/create.*token/i,/random.*string/i,/get.*random/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:5.3,confidence:"medium"},hasSuggestions:true,messages:{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"}),useRandomBytes:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use randomBytes",description:"Use crypto.randomBytes() for cryptographically secure random values",severity:"LOW",fix:'crypto.randomBytes(32).toString("hex")',documentationLink:"https://nodejs.org/api/crypto.html#cryptorandombytessize-callback"}),useRandomUUID:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use randomUUID",description:"Use crypto.randomUUID() for UUID generation",severity:"LOW",fix:"crypto.randomUUID()",documentationLink:"https://nodejs.org/api/crypto.html#cryptorandomuuidoptions"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow Math.random() in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);function isCryptoContext(node){let current=node.parent;while(current){if(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(CRYPTO_VARIABLE_PATTERNS.some(p=>p.test(varName))){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&current.id){const funcName=current.id.name;if(CRYPTO_FUNCTION_PATTERNS.some(p=>p.test(funcName))){return true}}if(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(CRYPTO_VARIABLE_PATTERNS.some(p=>p.test(propName))){return true}}}if(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(CRYPTO_VARIABLE_PATTERNS.some(p=>p.test(propName))){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(CRYPTO_FUNCTION_PATTERNS.some(p=>p.test(funcName))||CRYPTO_VARIABLE_PATTERNS.some(p=>p.test(funcName))){return true}}}}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.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.object.name==="Math"&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.name==="random"){if(isCryptoContext(node)){context.report({node,messageId:"mathRandomCrypto",suggest:[{messageId:"useRandomBytes",fix:()=>null},{messageId:"useRandomUUID",fix:()=>null}]})}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noMathRandomCrypto=void 0;const names_1=require("../../utils/names");const eslint_devkit_1=require("@interlace/eslint-devkit");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"];const nameSuggestsCrypto=(0,names_1.makeNameTest)(CRYPTO_WORDS);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:5.3,confidence:"medium"},hasSuggestions:true,messages:{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"}),useRandomBytes:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use randomBytes",description:"Use crypto.randomBytes() for cryptographically secure random values",severity:"LOW",fix:'crypto.randomBytes(32).toString("hex")',documentationLink:"https://nodejs.org/api/crypto.html#cryptorandombytessize-callback"}),useRandomUUID:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use randomUUID",description:"Use crypto.randomUUID() for UUID generation",severity:"LOW",fix:"crypto.randomUUID()",documentationLink:"https://nodejs.org/api/crypto.html#cryptorandomuuidoptions"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow Math.random() in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);function isCryptoContext(node){let current=node.parent;while(current){if(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)){return true}}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&current.id){const funcName=current.id.name;if(CRYPTO_FUNCTION_PATTERNS.some(p=>p.test(funcName))){return true}}if(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)){return true}}}if(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)){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(CRYPTO_FUNCTION_PATTERNS.some(p=>p.test(funcName))||nameSuggestsCrypto(funcName)){return true}}}}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.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.object.name==="Math"&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.property.name==="random"){if(isCryptoContext(node)){context.report({node,messageId:"mathRandomCrypto",suggest:[{messageId:"useRandomBytes",fix:()=>null},{messageId:"useRandomUUID",fix:()=>null}]})}}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noShellInjection=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");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",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:[]},defaultOptions:[],create(context){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;const firstArg=node.arguments[0];if(!firstArg||firstArg.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return;if(isStringConcatOrTemplate(firstArg)){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",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:[]},defaultOptions:[],create(context){const isLiteralConstant=(0,constant_folding_1.makeIsLiteralConstant)(context.sourceCode);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;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 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_OBJECTS=new Set(["axios","got","superagent","request","http","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){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 carriesUntrustedUrl(node){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:return isUserInputParamName(node.name);case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return isRequestSourced(node);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(carriesUntrustedUrl);case eslint_devkit_1.AST_NODE_TYPES.CallExpression:case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(carriesUntrustedUrl);case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return carriesUntrustedUrl(node.left)||carriesUntrustedUrl(node.right);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))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())&&carriesUntrustedUrl(value)});default:return false}}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}},additionalProperties:false}]},defaultOptions:[{allowInTests:true}],create(context,[options={}]){const{allowInTests=true}=options||{};const filename=context.filename;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(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_OBJECTS.has(node.callee.object.name)&&HTTP_CLIENT_METHODS.has(node.callee.property.name)){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_OBJECTS=new Set(["axios","got","superagent","request","http","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){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;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)||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))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&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(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_OBJECTS.has(node.callee.object.name)&&HTTP_CLIENT_METHODS.has(node.callee.property.name)){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 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 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)}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:7.5},hasSuggestions:true,messages:{timingUnsafeCompare:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Timing-unsafe comparison",cwe:"CWE-208",description:"Using === to compare secrets enables timing attacks. The comparison short-circuits on first mismatch, leaking information about the secret.",severity:"HIGH",fix:"Use crypto.timingSafeEqual() for constant-time comparison",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"}),useTimingSafeEqual:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use timingSafeEqual",description:"Use constant-time comparison to prevent timing attacks",severity:"LOW",fix:"crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS}=options;const patterns=secretPatterns.map(p=>new RegExp(p,"i"));function nameLooksSecret(name){if(BOOLEAN_PREDICATE_NAME.test(name))return false;return patterns.some(p=>p.test(name))}function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(node.name)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){const prop=node.property;if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(prop.name)}}return false}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}if(isSourceConstant(node.left)||isSourceConstant(node.right)){return}if(isNamedConstant(node.left)||isNamedConstant(node.right)){return}const leftIsSecret=isSecretIdentifier(node.left);const rightIsSecret=isSecretIdentifier(node.right);if(leftIsSecret||rightIsSecret){context.report({node,messageId:"timingUnsafeCompare",suggest:[{messageId:"useTimingSafeEqual",fix:()=>null}]})}}return{BinaryExpression:checkBinaryExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimingUnsafeCompare=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_SECRET_PATTERNS=["token","secret","password","hash","signature","mac","hmac","digest","apiKey","api_key","api-key","auth","credential","bearer","jwt","csrf","nonce","ssn","social_security","social-security","pii","private_key","private-key","privateKey","access_token","access-token","accessToken","refresh_token","refresh-token","refreshToken","session_id","session-id","sessionId","auth_token","auth-token","authToken","encryption_key","encryption-key","encryptionKey"];function isSourceConstant(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.name==="undefined")return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return true;if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral)return node.expressions.length===0;return false}const BOOLEAN_PREDICATE_NAME=/^(?:is|has|should|can|did|was|will|does)[A-Z]/;const NAMED_CONSTANT=/^[A-Z][A-Z0-9_]*$/;const DEFAULT_UNTRUSTED_SOURCES=["req","request","ctx","event"];const NAMESPACE_NAME=/^(?:[A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)$/;function isNamedConstant(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;if(node.computed)return false;if(node.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return NAMESPACE_NAME.test(node.object.name)&&NAMED_CONSTANT.test(node.property.name)}function memberRoot(node){let current=node;for(;;){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object;continue}if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){current=current.callee;continue}break}return current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?current:null}function isSelfComparison(left,right){const pair=(bare,derived)=>{if(bare.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;if(derived.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return memberRoot(derived)?.name===bare.name};return pair(left,right)||pair(right,left)}const NON_SECRET_MEMBERS=new Map([["hash",new Set(["location"])]]);function isNonSecretMember(node){if(node.computed||node.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const receivers=NON_SECRET_MEMBERS.get(node.property.name.toLowerCase());if(!receivers)return false;const owner=node.object;const ownerName=owner.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.name:owner.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!owner.computed&&owner.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.property.name:"";return receivers.has(ownerName.toLowerCase())}exports.noTimingUnsafeCompare=(0,eslint_devkit_1.createRule)({name:"no-timing-unsafe-compare",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-timing-unsafe-compare.md",description:"Disallow timing-unsafe comparison of secrets",cwe:"CWE-208",cvss:7.5},hasSuggestions:true,messages:{timingUnsafeCompare:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Timing-unsafe comparison",cwe:"CWE-208",description:"Using === to compare secrets enables timing attacks. The comparison short-circuits on first mismatch, leaking information about the secret.",severity:"HIGH",fix:"Use crypto.timingSafeEqual() for constant-time comparison",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"}),useTimingSafeEqual:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use timingSafeEqual",description:"Use constant-time comparison to prevent timing attacks",severity:"LOW",fix:"crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))",documentationLink:"https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b"})},schema:[{type:"object",properties:{secretPatterns:{type:"array",items:{type:"string"},default:DEFAULT_SECRET_PATTERNS,description:"Variable name patterns that indicate secrets"},untrustedSources:{type:"array",items:{type:"string"},default:DEFAULT_UNTRUSTED_SOURCES,description:"Identifier roots treated as attacker-controlled (default: req, request, ctx, event)"},reportUnverifiedComparisons:{type:"boolean",default:false,description:"Report on a secret-looking name alone, without an attacker-controlled operand. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{secretPatterns:DEFAULT_SECRET_PATTERNS}],create(context,[options={}]){const{secretPatterns=DEFAULT_SECRET_PATTERNS,untrustedSources=DEFAULT_UNTRUSTED_SOURCES,reportUnverifiedComparisons=false}=options;const sourceCode=context.sourceCode;const readsUntrusted=(0,provenance_1.makeReadsTaintSource)(sourceCode,new Set(untrustedSources.map(source=>source.toLowerCase())));const patterns=secretPatterns.map(p=>new RegExp(p,"i"));function nameLooksSecret(name){if(BOOLEAN_PREDICATE_NAME.test(name))return false;return patterns.some(p=>p.test(name))}function isSecretIdentifier(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(node.name)}if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){if(isNonSecretMember(node))return false;const prop=node.property;if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return nameLooksSecret(prop.name)}}return false}function isResolvedConstant(node){if(isSourceConstant(node))return true;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return(0,provenance_1.constLiteralOf)(sourceCode,node)!==void 0}function checkBinaryExpression(node){if(node.operator!=="==="&&node.operator!=="=="&&node.operator!=="!=="&&node.operator!=="!="){return}if(isResolvedConstant(node.left)||isResolvedConstant(node.right)){return}if(isNamedConstant(node.left)||isNamedConstant(node.right)){return}if(isSelfComparison(node.left,node.right)){return}const leftIsSecret=isSecretIdentifier(node.left);const rightIsSecret=isSecretIdentifier(node.right);if(leftIsSecret||rightIsSecret){if(!reportUnverifiedComparisons){const leftUntrusted=readsUntrusted(node.left);const rightUntrusted=readsUntrusted(node.right);if(leftUntrusted===rightUntrusted)return}context.report({node,messageId:"timingUnsafeCompare",suggest:[{messageId:"useTimingSafeEqual",fix:()=>null}]})}}return{BinaryExpression:checkBinaryExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noToctouVulnerability=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");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.5},hasSuggestions:true,messages:{toctouVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"TOCTOU vulnerability",cwe:"CWE-367",description:"Time-of-check Time-of-use race condition detected",severity:"HIGH",fix:"Use atomic operations or fs.promises for file operations",documentationLink:"https://cwe.mitre.org/data/definitions/367.html"}),useAtomicOperations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Atomic Operations",description:"Use atomic file operations",severity:"LOW",fix:"fs.promises.access() then fs.promises.readFile()",documentationLink:"https://nodejs.org/api/fs.html#fspromisesaccesspath-mode"}),useFsPromises:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use fs.promises",description:"Use fs.promises API",severity:"LOW",fix:"await fs.promises.readFile() instead of sync operations",documentationLink:"https://nodejs.org/api/fs.html#promises-api"}),addProperLocking:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Add File Locking",description:"Add proper locking mechanism",severity:"LOW",fix:"Use proper-lockfile or similar for concurrent access",documentationLink:"https://github.com/moxystudio/node-proper-lockfile"})},schema:[{type:"object",properties:{ignoreInTests:{type:"boolean",default:true},fsMethods:{type:"array",items:{type:"string"},default:["fs.existsSync","fs.statSync","fs.accessSync"],description:"Filesystem check calls that create a time-of-check window"}},additionalProperties:false}]},defaultOptions:[{ignoreInTests:true,fsMethods:["fs.existsSync","fs.statSync","fs.accessSync"]}],create(context,[options={}]){const{ignoreInTests=true}=options||{};const filename=context.filename;const isTestFile=ignoreInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);if(isTestFile){return{}}const sourceCode=context.sourceCode;function checkCallExpression(node){let useMethodName="";if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const objectName=node.callee.object.type==="Identifier"?node.callee.object.name:"";if(objectName==="fs"||objectName==="fsPromises"){useMethodName=node.callee.property.name}}else if(node.callee.type==="Identifier"){useMethodName=node.callee.name}const riskyUseMethods=["readFileSync","writeFileSync","readFile","writeFile","openSync","open","unlinkSync","unlink"];if(!riskyUseMethods.includes(useMethodName)){return}const useArg=node.arguments[0];if(!useArg)return;let current=node.parent;while(current){if(current.type==="IfStatement"){let condition=current.test;if(condition.type==="UnaryExpression"&&condition.operator==="!"){condition=condition.argument}if(condition.type==="CallExpression"){let checkMethodName="";if(condition.callee.type==="MemberExpression"&&condition.callee.property.type==="Identifier"){checkMethodName=condition.callee.property.name}else if(condition.callee.type==="Identifier"){checkMethodName=condition.callee.name}const checkMethods=["existsSync","statSync","accessSync","exists","stat","access"];if(checkMethods.includes(checkMethodName)){const checkArg=condition.arguments[0];if(checkArg){if(checkArg.type==="Identifier"&&useArg.type==="Identifier"&&checkArg.name===useArg.name){reportToctou(node);return}const checkArgText=sourceCode.getText(checkArg).replace(/\s/g,"");const useArgText=sourceCode.getText(useArg).replace(/\s/g,"");if(checkArgText===useArgText){reportToctou(node);return}}}if(condition.callee.type==="MemberExpression"&&condition.callee.property.type==="Identifier"&&["isFile","isDirectory"].includes(condition.callee.property.name)&&condition.callee.object.type==="Identifier"){const statsVarName=condition.callee.object.name;let currentScope=sourceCode.getScope(condition);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===statsVarName)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init&&def.node.init.type==="CallExpression"){const init=def.node.init;if(init.callee.type==="MemberExpression"&&init.callee.property.type==="Identifier"&&["statSync","lstatSync","stat","lstat"].includes(init.callee.property.name)){const statArg=init.arguments[0];if(statArg){const checkArgText=sourceCode.getText(statArg).replace(/\s/g,"");const useArgText=sourceCode.getText(useArg).replace(/\s/g,"");if(checkArgText===useArgText){reportToctou(node);return}}}}}}}}current=current.parent}}function reportToctou(node){context.report({node,messageId:"toctouVulnerability",suggest:[{messageId:"useAtomicOperations",fix:()=>null},{messageId:"useFsPromises",fix:()=>null},{messageId:"addProperLocking",fix:()=>null}]})}return{CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noToctouVulnerability=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const PER_USER_ROOT_FUNCTIONS=new Set(["homedir","userInfo"]);const PER_USER_ENV_VARS=new Set(["HOME","USERPROFILE","LOCALAPPDATA","APPDATA","XDG_CACHE_HOME","XDG_CONFIG_HOME","XDG_DATA_HOME","XDG_STATE_HOME"]);exports.noToctouVulnerability=(0,eslint_devkit_2.createRule)({name:"no-toctou-vulnerability",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-toctou-vulnerability.md",description:"Detects Time-of-Check-Time-of-Use vulnerabilities",cwe:"CWE-367",cvss:7.5},hasSuggestions:true,messages:{toctouVulnerability:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"TOCTOU vulnerability",cwe:"CWE-367",description:"Time-of-check Time-of-use race condition detected",severity:"HIGH",fix:"Use atomic operations or fs.promises for file operations",documentationLink:"https://cwe.mitre.org/data/definitions/367.html"}),useAtomicOperations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Atomic Operations",description:"Use atomic file operations",severity:"LOW",fix:"fs.promises.access() then fs.promises.readFile()",documentationLink:"https://nodejs.org/api/fs.html#fspromisesaccesspath-mode"}),useFsPromises:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use fs.promises",description:"Use fs.promises API",severity:"LOW",fix:"await fs.promises.readFile() instead of sync operations",documentationLink:"https://nodejs.org/api/fs.html#promises-api"}),addProperLocking:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Add File Locking",description:"Add proper locking mechanism",severity:"LOW",fix:"Use proper-lockfile or similar for concurrent access",documentationLink:"https://github.com/moxystudio/node-proper-lockfile"})},schema:[{type:"object",properties:{ignoreInTests:{type:"boolean",default:true},fsMethods:{type:"array",items:{type:"string"},default:["fs.existsSync","fs.statSync","fs.accessSync"],description:"Filesystem check calls that create a time-of-check window"}},additionalProperties:false}]},defaultOptions:[{ignoreInTests:true,fsMethods:["fs.existsSync","fs.statSync","fs.accessSync"]}],create(context,[options={}]){const{ignoreInTests=true}=options||{};const filename=context.filename;const isTestFile=ignoreInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);if(isTestFile){return{}}const sourceCode=context.sourceCode;function returnedExpression(callee){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const variable=(0,provenance_1.findVariable)(sourceCode,callee);if(!variable||variable.defs.length!==1)return null;const def=variable.defs[0];if(def.type!=="FunctionName")return null;const body=def.node.body;if(body?.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return null;for(const statement of body.body){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&statement.argument){return statement.argument}}return null}function reachesPerUserRoot(node,depth=0){if(depth>8)return false;const next=child=>reachesPerUserRoot(child,depth+1);switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const variable=(0,provenance_1.findVariable)(sourceCode,node);const def=variable?.defs.length===1?variable.defs[0]:void 0;if(def?.type!=="Variable"||!def.node.init)return false;return next(def.node.init)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:{if(!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&PER_USER_ENV_VARS.has(node.property.name)){return true}return next(node.object)}case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const callee=node.callee;const name=callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.name:callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?callee.property.name:null;if(name!==null&&PER_USER_ROOT_FUNCTIONS.has(name))return true;if(node.arguments.some(argument=>next(argument)))return true;const returned=returnedExpression(callee);return returned!==null&&next(returned)}case eslint_devkit_1.AST_NODE_TYPES.ConditionalExpression:return next(node.consequent)||next(node.alternate);case eslint_devkit_1.AST_NODE_TYPES.LogicalExpression:return next(node.left)||next(node.right);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.some(expression=>next(expression));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return next(node.left)||next(node.right);default:return false}}const EXISTENCE_CHECKS=new Set(["existsSync","exists"]);const READ_ONLY_USES=new Set(["readFileSync","readFile"]);function isSecurityRelevantWindow(checkMethod,useMethod){if(!EXISTENCE_CHECKS.has(checkMethod))return true;return!READ_ONLY_USES.has(useMethod)}function checkCallExpression(node){let useMethodName="";if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const objectName=node.callee.object.type==="Identifier"?node.callee.object.name:"";if(objectName==="fs"||objectName==="fsPromises"){useMethodName=node.callee.property.name}}else if(node.callee.type==="Identifier"){useMethodName=node.callee.name}const riskyUseMethods=["readFileSync","writeFileSync","readFile","writeFile","openSync","open","unlinkSync","unlink"];if(!riskyUseMethods.includes(useMethodName)){return}const useArg=node.arguments[0];if(!useArg)return;if(reachesPerUserRoot(useArg))return;let current=node.parent;while(current){if(current.type==="IfStatement"){let condition=current.test;if(condition.type==="UnaryExpression"&&condition.operator==="!"){condition=condition.argument}if(condition.type==="CallExpression"){let checkMethodName="";if(condition.callee.type==="MemberExpression"&&condition.callee.property.type==="Identifier"){checkMethodName=condition.callee.property.name}else if(condition.callee.type==="Identifier"){checkMethodName=condition.callee.name}const checkMethods=["existsSync","statSync","accessSync","exists","stat","access"];if(checkMethods.includes(checkMethodName)&&isSecurityRelevantWindow(checkMethodName,useMethodName)){const checkArg=condition.arguments[0];if(checkArg){if(checkArg.type==="Identifier"&&useArg.type==="Identifier"&&checkArg.name===useArg.name){reportToctou(node);return}const checkArgText=sourceCode.getText(checkArg).replace(/\s/g,"");const useArgText=sourceCode.getText(useArg).replace(/\s/g,"");if(checkArgText===useArgText){reportToctou(node);return}}}if(condition.callee.type==="MemberExpression"&&condition.callee.property.type==="Identifier"&&["isFile","isDirectory"].includes(condition.callee.property.name)&&condition.callee.object.type==="Identifier"){const statsVarName=condition.callee.object.name;let currentScope=sourceCode.getScope(condition);let variable=null;while(currentScope){variable=currentScope.variables.find(v=>v.name===statsVarName)||null;if(variable)break;currentScope=currentScope.upper}if(variable&&variable.defs.length>0){const def=variable.defs[0];if(def.type==="Variable"&&def.node.init&&def.node.init.type==="CallExpression"){const init=def.node.init;if(init.callee.type==="MemberExpression"&&init.callee.property.type==="Identifier"&&["statSync","lstatSync","stat","lstat"].includes(init.callee.property.name)){const statArg=init.arguments[0];if(statArg){const checkArgText=sourceCode.getText(statArg).replace(/\s/g,"");const useArgText=sourceCode.getText(useArg).replace(/\s/g,"");if(checkArgText===useArgText){reportToctou(node);return}}}}}}}}current=current.parent}}function reportToctou(node){context.report({node,messageId:"toctouVulnerability",suggest:[{messageId:"useAtomicOperations",fix:()=>null},{messageId:"useFsPromises",fix:()=>null},{messageId:"addProperLocking",fix:()=>null}]})}return{CallExpression:checkCallExpression}}});
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnboundedDecompression=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const ZLIB_MODULE=/^(node:)?zlib$/;const ASYNC_DECOMPRESSORS=new Set(["gunzip","inflate","inflateRaw","unzip","brotliDecompress","zstdDecompress"]);const SYNC_DECOMPRESSORS=new Set(["gunzipSync","inflateSync","inflateRawSync","unzipSync","brotliDecompressSync","zstdDecompressSync"]);exports.noUnboundedDecompression=(0,eslint_devkit_1.createRule)({name:"no-unbounded-decompression",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-unbounded-decompression.md",description:"Require a maxOutputLength ceiling on zlib one-shot decompression",cwe:"CWE-409",cvss:7.5},hasSuggestions:false,messages:{unboundedDecompression:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unbounded decompression",cwe:"CWE-409",description:"zlib.{{fn}}() buffers the whole decompressed result in memory with no maxOutputLength cap. A few KB of crafted input can expand to gigabytes and exhaust the heap (decompression bomb).",severity:"HIGH",fix:"Pass an explicit ceiling: zlib.{{fn}}(input, { maxOutputLength: 10 * 1024 * 1024 }, \u2026)",documentationLink:"https://nodejs.org/api/zlib.html#class-options"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow unbounded decompression in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(context.filename);const namespaceBindings=new Set;const directBindings=new Map;const pending=[];function noteDirect(local,imported){if(ASYNC_DECOMPRESSORS.has(imported)||SYNC_DECOMPRESSORS.has(imported)){directBindings.set(local,imported)}}function requiredModule(init){if(init.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&init.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&init.callee.name==="require"&&init.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof init.arguments[0].value==="string"){return init.arguments[0].value}return null}function decompressorName(node){const callee=node.callee;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!callee.computed&&namespaceBindings.has(callee.object.name)){const name=callee.property.name;return ASYNC_DECOMPRESSORS.has(name)||SYNC_DECOMPRESSORS.has(name)?name:null}if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return directBindings.get(callee.name)??null}return null}function isLiteralPayload(argument){if(argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return true;return argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&argument.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&argument.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&argument.callee.object.name==="Buffer"&&argument.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.Literal}function outputCap(options_){let capped=false;for(const property of options_.properties){if(property.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return"unknown";if(property.computed)continue;const named=property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?property.key.name:property.key.value;if(named==="maxOutputLength")capped=true}return capped?"capped":"uncapped"}function judge(node){const fn=decompressorName(node);if(fn===null)return;const args=node.arguments;const payload=args[0];if(payload===void 0)return;if(isLiteralPayload(payload))return;const isAsync=ASYNC_DECOMPRESSORS.has(fn);if(isAsync&&args.length<2)return;const candidates=isAsync?args.slice(1,args.length-1):args.slice(1);if(candidates.length>1)return;const optionsArgument=candidates[0];if(optionsArgument!==void 0){if(optionsArgument.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;if(outputCap(optionsArgument)!=="uncapped")return}context.report({node,messageId:"unboundedDecompression",data:{fn}})}return{ImportDeclaration(node){if(!ZLIB_MODULE.test(node.source.value))return;for(const specifier of node.specifiers){if(specifier.type===eslint_devkit_1.AST_NODE_TYPES.ImportSpecifier){const imported=specifier.imported.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?specifier.imported.name:specifier.imported.value;noteDirect(specifier.local.name,imported);continue}namespaceBindings.add(specifier.local.name)}},VariableDeclarator(node){if(!node.init)return;const source=requiredModule(node.init);if(source===null||!ZLIB_MODULE.test(source))return;if(node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){namespaceBindings.add(node.id.name);return}if(node.id.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectPattern)return;for(const property of node.id.properties){if(property.type!==eslint_devkit_1.AST_NODE_TYPES.Property)continue;if(property.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&property.value.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){noteDirect(property.value.name,property.key.name)}}},CallExpression(node){if(isTestFile)return;pending.push({node})},"Program:exit"(){for(const{node}of pending)judge(node)}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeBufferAlloc=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const UNSAFE_ALLOCATORS=new Set(["allocUnsafe","allocUnsafeSlow"]);exports.noUnsafeBufferAlloc=(0,eslint_devkit_1.createRule)({name:"no-unsafe-buffer-alloc",meta:{type:"problem",hasSuggestions:true,docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-unsafe-buffer-alloc.md",description:"Disallow `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which return uninitialized memory",cwe:"CWE-908",cweJustification:"CWE-908 (Use of Uninitialized Resource) \u2014 allocUnsafe returns a view over non-zeroed heap memory; any byte not overwritten before the buffer is read or transmitted discloses prior process memory.",cvss:7.5,confidence:"high"},messages:{unsafeAlloc:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Uninitialized Buffer Allocation",cwe:"CWE-908",cvss:7.5,description:"`Buffer.allocUnsafe(size)` returns memory that has not been zeroed. Every byte not overwritten before the buffer is read or sent leaks whatever the allocator previously stored there.",severity:"HIGH",fix:"Use `Buffer.alloc(size)` (zero-filled), or keep `allocUnsafe` only where the very next statement overwrites the whole buffer \u2014 `Buffer.allocUnsafe(size).fill(0)` is accepted by this rule.",documentationLink:"https://nodejs.org/api/buffer.html#static-method-bufferallocunsafesize"}),unsafeAllocSlow:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Uninitialized Buffer Allocation (allocUnsafeSlow)",cwe:"CWE-908",cvss:7.5,description:"`Buffer.allocUnsafeSlow(size)` allocates outside the shared pool but is equally uninitialized \u2014 the returned bytes are whatever was last in that memory.",severity:"HIGH",fix:"Use `Buffer.alloc(size)`, or append `.fill(0)` to zero the allocation at the call site.",documentationLink:"https://nodejs.org/api/buffer.html#static-method-bufferallocunsafeslowsize"}),useSafeAlloc:"Replace with `Buffer.alloc()` (zero-filled)."},schema:[]},defaultOptions:[],create(context){function isFilledInPlace(node){const parent=node.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||parent.object!==node||parent.computed||parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||parent.property.name!=="fill"){return false}const grandparent=parent.parent;return grandparent?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&grandparent.callee===parent}return{CallExpression(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||callee.object.name!=="Buffer"||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||!UNSAFE_ALLOCATORS.has(callee.property.name)){return}if(isFilledInPlace(node))return;context.report({node,messageId:callee.property.name==="allocUnsafe"?"unsafeAlloc":"unsafeAllocSlow",suggest:[{messageId:"useSafeAlloc",fix:fixer=>fixer.replaceText(callee.property,"alloc")}]})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeBufferAlloc=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const UNSAFE_ALLOCATORS=new Set(["allocUnsafe","allocUnsafeSlow"]);const SIZED_ALLOCATORS=new Set(["Array","Buffer","Uint8Array","Uint16Array","Uint32Array","Int8Array","Int16Array","Int32Array","Float32Array","Float64Array","BigInt64Array","BigUint64Array","ArrayBuffer","SharedArrayBuffer"]);function looksNumeric(node){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Literal:return typeof node.value==="number";case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return["+","-","*","/","%","<<",">>",">>>"].includes(node.operator);case eslint_devkit_1.AST_NODE_TYPES.Identifier:return COUNT_NAMES.has(node.name.toLowerCase());case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&COUNT_NAMES.has(node.property.name.toLowerCase());case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.object.name==="Math"){return true}const name=calleeName(node.callee);return name!==null&&/^(read|len|size|count|decode|parse)/i.test(name)}default:return false}}const COUNT_NAMES=new Set(["length","len","size","count","n","num","total","capacity","bytelength"]);const BUFFER_ALLOCATORS=new Set(["alloc","allocUnsafe","allocUnsafeSlow"]);const WIRE_NAMES=new Set(["chunk","chunks","buffer","buf","data","payload","frame","packet","bytes","raw","message","msg"]);const REQUEST_ROOTS=new Set(["req","request","ctx","event"]);function isWriteMethod(name){return name==="fill"||name==="set"||name.startsWith("write")}const DESTINATION_ARGUMENT_CALLS=new Set(["copy","randomFill","randomFillSync"]);const METADATA_PROPERTIES=new Set(["length","byteLength","byteOffset","buffer"]);function isInsideLoop(node){let current=node.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.ForStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.ForOfStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.ForInStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.WhileStatement||current.type===eslint_devkit_1.AST_NODE_TYPES.DoWhileStatement){return true}current=current.parent}return false}function coversWholeBuffer(call,method){if(method==="fill")return true;if(call.arguments.length===1)return true;const offset=call.arguments[1];return offset.type!==eslint_devkit_1.AST_NODE_TYPES.Literal&&isInsideLoop(call)}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&&!callee.computed){return callee.property.name}return null}function declaredName(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration&&node.id)return node.id.name;const parent=node.parent;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator){return parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?parent.id.name:null}if((parent?.type===eslint_devkit_1.AST_NODE_TYPES.MethodDefinition||parent?.type===eslint_devkit_1.AST_NODE_TYPES.Property)&&!parent.computed){const key=parent.key;return key.name}return null}exports.noUnsafeBufferAlloc=(0,eslint_devkit_1.createRule)({name:"no-unsafe-buffer-alloc",meta:{type:"problem",hasSuggestions:true,docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-unsafe-buffer-alloc.md",description:"Disallow `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which return uninitialized memory",cwe:"CWE-908",cweJustification:"CWE-908 (Use of Uninitialized Resource) \u2014 allocUnsafe returns a view over non-zeroed heap memory; any byte not overwritten before the buffer is read or transmitted discloses prior process memory.",cvss:7.5,confidence:"high"},messages:{unsafeAlloc:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Uninitialized Buffer Allocation",cwe:"CWE-908",cvss:7.5,description:"`Buffer.allocUnsafe(size)` returns memory that has not been zeroed. Every byte not overwritten before the buffer is read or sent leaks whatever the allocator previously stored there.",severity:"HIGH",fix:"Use `Buffer.alloc(size)` (zero-filled), or keep `allocUnsafe` only where the very next statement overwrites the whole buffer \u2014 `Buffer.allocUnsafe(size).fill(0)` is accepted by this rule.",documentationLink:"https://nodejs.org/api/buffer.html#static-method-bufferallocunsafesize"}),unsafeAllocSlow:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Uninitialized Buffer Allocation (allocUnsafeSlow)",cwe:"CWE-908",cvss:7.5,description:"`Buffer.allocUnsafeSlow(size)` allocates outside the shared pool but is equally uninitialized \u2014 the returned bytes are whatever was last in that memory.",severity:"HIGH",fix:"Use `Buffer.alloc(size)`, or append `.fill(0)` to zero the allocation at the call site.",documentationLink:"https://nodejs.org/api/buffer.html#static-method-bufferallocunsafeslowsize"}),useSafeAlloc:"Replace with `Buffer.alloc()` (zero-filled).",unboundedAllocation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Allocation Sized By Untrusted Input",cwe:"CWE-789",cvss:7.5,description:'The allocation size is read off the wire, so the peer picks it. For `new Array(n)` the hazard is a narrow band, not "large n": V8 keeps a packed backing store only up to ~33.5M elements, and a length just under that turns a 12-byte length prefix into a 229MB allocation (measured: `new Array(3e7)` = 228.9MB in 20.7ms). Past the threshold V8 switches the array to dictionary mode and the allocation costs nothing at all (`new Array(4e7)` and `new Array(1e9)` are both 0.0MB in ~0.005ms). A typed allocation \u2014 `Buffer.alloc(n)`, `new Uint8Array(n)` \u2014 has no such threshold and commits n bytes at any n.',severity:"HIGH",fix:'Clamp the length against the maximum the protocol actually permits, before allocating: `if (length > MAX_LENGTH) throw new Error("too long")`, or `new Array(Math.min(length, MAX_LENGTH))`. A guard that only rejects implausibly huge values is not a fix for `new Array` \u2014 those are the sizes V8 makes free. The damaging lengths are the plausible ones just below the packed-elements limit.',documentationLink:"https://cwe.mitre.org/data/definitions/789.html"}),clampAllocation:"Clamp the size against the maximum the protocol permits."},schema:[]},defaultOptions:[],create(context){const wireParams=new Map;const pendingAllocations=[];const pendingCallSites=[];const bindings=new Map;function readsWire(node,depth=0){if(depth>10)return false;switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const lower=node.name.toLowerCase();if(WIRE_NAMES.has(lower)||REQUEST_ROOTS.has(lower))return true;const owner=enclosingFunction(node);if(owner!==null){const name=declaredName(owner);const indices=name===null?void 0:wireParams.get(name);if(indices!==void 0){const index=paramIndexOf(node,owner);if(index!==null&&indices.has(index))return true}}const bound=bindings.get(node.name);return bound!==void 0&&readsWire(bound,depth+1)}case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return readsWire(node.object,depth+1);case eslint_devkit_1.AST_NODE_TYPES.NewExpression:return node.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsWire(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return readsWire(node.callee,depth+1)||node.arguments.some(argument=>argument.type!==eslint_devkit_1.AST_NODE_TYPES.SpreadElement&&readsWire(argument,depth+1));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return readsWire(node.left,depth+1)||readsWire(node.right,depth+1);default:return false}}function enclosingFunction(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}function paramIndexOf(node,owner){const params=owner.params;const index=params.findIndex(param=>param.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&param.name===node.name);return index===-1?null:index}function isClamped(size){if(size.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&size.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&size.callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&size.callee.object.name==="Math"&&size.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&size.callee.property.name==="min"){return true}if(size.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const name=size.name;let current=size.parent;while(current){if(current.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement){if(mentionsInComparison(current.test,name))return true}if(current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||current.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression){const body=current.body;if(body.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){for(const statement of body.body){if(statement.type===eslint_devkit_1.AST_NODE_TYPES.IfStatement&&mentionsInComparison(statement.test,name)){return true}}}return false}current=current.parent}return false}function mentionsInComparison(test,name){if(test.type===eslint_devkit_1.AST_NODE_TYPES.LogicalExpression){return mentionsInComparison(test.left,name)||mentionsInComparison(test.right,name)}if(test.type!==eslint_devkit_1.AST_NODE_TYPES.BinaryExpression)return false;if(!["<","<=",">",">="].includes(test.operator))return false;const named=side=>side.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&side.name===name;return named(test.left)||named(test.right)}function allocationSize(node){const size=node.arguments[0];if(size===void 0||size.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return null;const callee=node.callee;if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression&&callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&SIZED_ALLOCATORS.has(callee.name)){return looksNumeric(size)?size:null}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==="Buffer"&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&BUFFER_ALLOCATORS.has(callee.property.name)){return size}return null}function recordCallSite(node){const name=calleeName(node.callee);if(name===null)return;node.arguments.forEach((argument,index)=>{if(argument.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return;if(!readsWire(argument))return;const indices=wireParams.get(name)??new Set;indices.add(index);wireParams.set(name,indices)})}function judgeAllocation(node,size){if(isClamped(size))return;if(!readsWire(size))return;context.report({node,messageId:"unboundedAllocation",suggest:[{messageId:"clampAllocation",fix:()=>null}]})}function classifyUse(identifier){const parent=identifier.parent;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===identifier){if(parent.computed){const grandparent2=parent.parent;return grandparent2.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&grandparent2.left===parent?"partial":"read"}if(parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return"read";const name=parent.property.name;if(METADATA_PROPERTIES.has(name))return"metadata";const grandparent=parent.parent;if(grandparent.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression||grandparent.callee!==parent||!isWriteMethod(name)){return"read"}return coversWholeBuffer(grandparent,name)?"covering":"partial"}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&parent.arguments[0]===identifier){const callee=parent.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||!DESTINATION_ARGUMENT_CALLS.has(callee.property.name)){return"read"}return coversWholeBuffer(parent,callee.property.name)?"covering":"partial"}return"read"}function isCoveredBeforeRead(call){const declarator=call.parent;if(declarator.type!==eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator||declarator.init!==call||declarator.id.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return false}const variable=context.sourceCode.getDeclaredVariables(declarator)[0];const uses=variable.references.filter(reference=>reference.identifier!==declarator.id).sort((a,b)=>a.identifier.range[0]-b.identifier.range[0]);for(const use of uses){const kind=classifyUse(use.identifier);if(kind==="metadata"||kind==="partial")continue;return kind==="covering"}return false}function isFilledInPlace(node){const parent=node.parent;if(parent?.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||parent.object!==node||parent.computed||parent.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||parent.property.name!=="fill"){return false}const grandparent=parent.parent;return grandparent?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&grandparent.callee===parent}return{VariableDeclarator(node){if(node.init!==null&&node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){bindings.set(node.id.name,node.init)}},NewExpression(node){pendingCallSites.push(node);const size=allocationSize(node);if(size!==null)pendingAllocations.push({node,size})},"Program:exit"(){for(const call of pendingCallSites)recordCallSite(call);for(const{node,size}of pendingAllocations)judgeAllocation(node,size)},CallExpression(node){pendingCallSites.push(node);const size=allocationSize(node);if(size!==null)pendingAllocations.push({node,size});const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.object.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||callee.object.name!=="Buffer"||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||!UNSAFE_ALLOCATORS.has(callee.property.name)){return}if(isFilledInPlace(node))return;if(isCoveredBeforeRead(node))return;context.report({node,messageId:callee.property.name==="allocUnsafe"?"unsafeAlloc":"unsafeAllocSlow",suggest:[{messageId:"useSafeAlloc",fix:fixer=>fixer.replaceText(callee.property,"alloc")}]})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeDynamicRequire=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");exports.noUnsafeDynamicRequire=(0,eslint_devkit_2.createRule)({name:"no-unsafe-dynamic-require",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-unsafe-dynamic-require.md",description:"Prevent unsafe dynamic require() calls that could enable code injection",cwe:"CWE-95",cvss:9.8},hasSuggestions:false,messages:{unsafeDynamicRequire:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Dynamic require()",cwe:"CWE-95",description:"Dynamic require() detected",severity:"CRITICAL",fix:'Use allowlist: const ALLOWED = ["mod1", "mod2"]; if (!ALLOWED.includes(name)) throw Error("Not allowed")',documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"})},schema:[{type:"object",properties:{allowDynamicImport:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{allowDynamicImport:false}],create(context){const requireVariables=new Set;const isRequireReference=node=>{if(node.type==="Identifier"&&node.name==="require"){return true}if(node.type==="Identifier"&&requireVariables.has(node.name)){return true}return false};const isDynamicArgument=arg=>{if(arg.type==="Literal")return false;if(arg.type==="TemplateLiteral"&&arg.expressions.length===0)return false;return true};return{VariableDeclarator(node){if(node.id.type==="Identifier"&&node.init){if(node.init.type==="Identifier"&&node.init.name==="require"){requireVariables.add(node.id.name)}}},CallExpression(node){if(node.callee.type!=="Identifier"){return}if(!isRequireReference(node.callee)){return}if(node.arguments.length===0)return;const firstArg=node.arguments[0];if(firstArg.type==="SpreadElement")return;if(!isDynamicArgument(firstArg))return;context.report({node,messageId:"unsafeDynamicRequire"})}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeDynamicRequire=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const provenance_1=require("../../utils/provenance");const DEFAULT_TAINT_SOURCES=["req","request","ctx","event","process"];exports.noUnsafeDynamicRequire=(0,eslint_devkit_2.createRule)({name:"no-unsafe-dynamic-require",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-unsafe-dynamic-require.md",description:"Prevent unsafe dynamic require() calls that could enable code injection",cwe:"CWE-95",cvss:9.8},hasSuggestions:false,messages:{unsafeDynamicRequire:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Dynamic require()",cwe:"CWE-95",description:"Dynamic require() detected",severity:"CRITICAL",fix:'Use allowlist: const ALLOWED = ["mod1", "mod2"]; if (!ALLOWED.includes(name)) throw Error("Not allowed")',documentationLink:"https://owasp.org/www-community/attacks/Code_Injection"})},schema:[{type:"object",properties:{allowDynamicImport:{type:"boolean",default:false},taintSources:{type:"array",items:{type:"string"},default:DEFAULT_TAINT_SOURCES,description:"Identifier roots treated as attacker-reachable (default: req, request, ctx, event, process)"},reportUnresolvedSpecifiers:{type:"boolean",default:false,description:'Report specifiers whose provenance cannot be resolved. Restores the pre-inversion "any non-literal is dangerous" behaviour.'}},additionalProperties:false}]},defaultOptions:[{allowDynamicImport:false}],create(context){const options=context.options[0]??{};const readsTaintSource=(0,provenance_1.makeReadsTaintSource)(context.sourceCode,new Set((options.taintSources??DEFAULT_TAINT_SOURCES).map(s=>s.toLowerCase())));const reportUnresolvedSpecifiers=options.reportUnresolvedSpecifiers??false;const requireVariables=new Set;const isRequireReference=node=>{if(node.type==="Identifier"&&node.name==="require"){return true}if(node.type==="Identifier"&&requireVariables.has(node.name)){return true}return false};const isDangerousSpecifier=arg=>{if(arg.type==="Literal")return false;if(arg.type==="TemplateLiteral"&&arg.expressions.length===0)return false;if(readsTaintSource(arg))return true;return reportUnresolvedSpecifiers};return{VariableDeclarator(node){if(node.id.type==="Identifier"&&node.init){if(node.init.type==="Identifier"&&node.init.name==="require"){requireVariables.add(node.id.name)}}},CallExpression(node){if(node.callee.type!=="Identifier"){return}if(!isRequireReference(node.callee)){return}if(node.arguments.length===0)return;const firstArg=node.arguments[0];if(firstArg.type==="SpreadElement")return;if(!isDangerousSpecifier(firstArg))return;context.report({node,messageId:"unsafeDynamicRequire"})}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noWeakHashAlgorithm=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const WEAK_HASH_PATTERNS=[{pattern:/\bmd5\b/i,name:"MD5",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bmd4\b/i,name:"MD4",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bsha1\b/i,name:"SHA-1",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bripemd\b/i,name:"RIPEMD",alternatives:["SHA-256","SHA-512"],replacement:"sha256"}];const DEFAULT_NON_CRYPTOGRAPHIC_NAMES=["sha","etag","cachekey","cachebuster"];function normalizeName(name){return name.replaceAll(/[_-]/g,"").toLowerCase()}function assignedName(node){let current=node;let parent=current.parent;while(parent){if(parent.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===current){current=parent;parent=current.parent;continue}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&parent.callee===current){current=parent;parent=current.parent;continue}break}if(!parent)return null;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.init===current){return parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?parent.id.name:null}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&parent.right===current){const target=parent.left;if(target.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return target.name;if(target.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!target.computed&&target.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return target.property.name}return null}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.Property&&parent.value===current){if(parent.computed)return null;if(parent.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return parent.key.name;if(parent.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof parent.key.value==="string"){return parent.key.value}return null}return null}function findWeakHash(value,additionalPatterns){for(const pattern of WEAK_HASH_PATTERNS){if(pattern.pattern.test(value)){return pattern}}for(const additionalPattern of additionalPatterns){const regex=new RegExp(`\\b${additionalPattern}\\b`,"i");if(regex.test(value)){return{pattern:regex,name:additionalPattern.toUpperCase(),alternatives:["SHA-256","SHA-512"],replacement:"sha256"}}}return null}exports.noWeakHashAlgorithm=(0,eslint_devkit_1.createRule)({name:"no-weak-hash-algorithm",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-weak-hash-algorithm.md",description:"Disallow weak hash algorithms (MD5, SHA1, MD4)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{weakHashAlgorithm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Weak hash algorithm",cwe:"CWE-327",description:"Use of weak hash algorithm: {{algorithm}}. {{algorithm}} is cryptographically broken and unsuitable for security purposes.",severity:"CRITICAL",fix:'Replace with {{replacement}}: crypto.createHash("{{replacement}}").update(data). If this hash is an identifier rather than a security control \u2014 an EVALSHA key, an ETag, a cache key \u2014 store it under one of the nonCryptographicNames instead.',documentationLink:"https://owasp.org/www-community/vulnerabilities/Weak_Cryptography"}),useSha256:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-256",description:"Replace with SHA-256 for secure hashing",severity:"LOW",fix:'crypto.createHash("sha256").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"}),useSha512:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-512",description:"Replace with SHA-512 for stronger hashing",severity:"LOW",fix:'crypto.createHash("sha512").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"}),useSha3:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-3",description:"Replace with SHA-3 for latest standard",severity:"LOW",fix:'crypto.createHash("sha3-256").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"})},schema:[{type:"object",properties:{additionalWeakAlgorithms:{type:"array",items:{type:"string"},default:[],description:"Additional weak algorithms to detect"},allowInTests:{type:"boolean",default:false,description:"Allow weak hashes in test files"},nonCryptographicNames:{type:"array",items:{type:"string"},default:DEFAULT_NON_CRYPTOGRAPHIC_NAMES,description:"Assignment target names that mark a hash as an identifier rather than a security control"}},additionalProperties:false}]},defaultOptions:[{additionalWeakAlgorithms:[],allowInTests:false,nonCryptographicNames:DEFAULT_NON_CRYPTOGRAPHIC_NAMES}],create(context,[options={}]){const{additionalWeakAlgorithms=[],allowInTests=false,nonCryptographicNames=DEFAULT_NON_CRYPTOGRAPHIC_NAMES}=options;const filename=context.filename;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);const nonCryptoNames=new Set(nonCryptographicNames.map(normalizeName));function isNonCryptographicUse(node){const name=assignedName(node);return name!==null&&nonCryptoNames.has(normalizeName(name))}function checkCallExpression(node){if(isTestFile)return;if(isNonCryptographicUse(node))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==="createHash"){checkHashArgument(node)}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="createHash"){checkHashArgument(node)}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const funcName=node.callee.name.toLowerCase();if(funcName==="sha1"||funcName==="md5"||funcName==="md4"){const weakPattern=findWeakHash(funcName,additionalWeakAlgorithms);context.report({node,messageId:"weakHashAlgorithm",data:{algorithm:weakPattern.name,replacement:weakPattern.replacement},suggest:[{messageId:"useSha256",fix:fixer=>{if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return fixer.replaceText(node.callee,"sha256")}return null}}]})}}}function checkHashArgument(node){for(const arg of node.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof arg.value==="string"){const weakPattern=findWeakHash(arg.value,additionalWeakAlgorithms);if(weakPattern){context.report({node:arg,messageId:"weakHashAlgorithm",data:{algorithm:weakPattern.name,replacement:weakPattern.replacement},suggest:[{messageId:"useSha256",fix:fixer=>fixer.replaceText(arg,`"sha256"`)},{messageId:"useSha512",fix:fixer=>fixer.replaceText(arg,`"sha512"`)},{messageId:"useSha3",fix:fixer=>fixer.replaceText(arg,`"sha3-256"`)}]})}}}}return{CallExpression:checkCallExpression}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noWeakHashAlgorithm=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const names_1=require("../../utils/names");const WEAK_HASH_PATTERNS=[{pattern:/\bmd5\b/i,name:"MD5",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bmd4\b/i,name:"MD4",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bsha1\b/i,name:"SHA-1",alternatives:["SHA-256","SHA-512","SHA-3"],replacement:"sha256"},{pattern:/\bripemd\b/i,name:"RIPEMD",alternatives:["SHA-256","SHA-512"],replacement:"sha256"}];const DEFAULT_NON_CRYPTOGRAPHIC_NAMES=["sha","etag","cachekey","cachebuster"];const DEFAULT_SECURITY_USE_NAMES=["password","passwd","secret","secrets","token","tokens","signature","signing","signed","sign","hmac","credential","credentials","certificate","cert","certs","apikey","privatekey","secretkey","signingkey","encryptionkey","session","csrf","salt","jwt","nonce","integrity","auth","authorization","authenticate"];function normalizeName(name){return name.replaceAll(/[_-]/g,"").toLowerCase()}function assignedName(node){let current=node;let parent=current.parent;while(parent){if(parent.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===current){current=parent;parent=current.parent;continue}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&parent.callee===current){current=parent;parent=current.parent;continue}break}if(!parent)return null;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.init===current){return parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?parent.id.name:null}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.AssignmentExpression&&parent.right===current){const target=parent.left;if(target.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return target.name;if(target.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!target.computed&&target.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return target.property.name}return null}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.Property&&parent.value===current){if(parent.computed)return null;if(parent.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return parent.key.name;if(parent.key.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof parent.key.value==="string"){return parent.key.value}return null}return null}function expressionName(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return node.name;if(node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!node.computed&&node.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return node.property.name}return null}function hashInputNames(node){const names=[];for(const argument of node.arguments){const name=expressionName(argument);if(name!==null)names.push(name)}let current=node;let parent=current.parent;while(parent){if(parent.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===current){current=parent;parent=current.parent;continue}if(parent.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&parent.callee===current){for(const argument of parent.arguments){const name=expressionName(argument);if(name!==null)names.push(name)}current=parent;parent=current.parent;continue}break}return names}function enclosingFunctionName(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){if(current.type!==eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&current.id){return current.id.name}const owner=current.parent;if(owner?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator){return owner.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?owner.id.name:null}if((owner?.type===eslint_devkit_1.AST_NODE_TYPES.Property||owner?.type===eslint_devkit_1.AST_NODE_TYPES.MethodDefinition)&&!owner.computed){return expressionName(owner.key)}return null}current=current.parent}return null}function findWeakHash(value,additionalPatterns){for(const pattern of WEAK_HASH_PATTERNS){if(pattern.pattern.test(value)){return pattern}}for(const additionalPattern of additionalPatterns){const regex=new RegExp(`\\b${additionalPattern}\\b`,"i");if(regex.test(value)){return{pattern:regex,name:additionalPattern.toUpperCase(),alternatives:["SHA-256","SHA-512"],replacement:"sha256"}}}return null}exports.noWeakHashAlgorithm=(0,eslint_devkit_1.createRule)({name:"no-weak-hash-algorithm",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-weak-hash-algorithm.md",description:"Disallow weak hash algorithms (MD5, SHA1, MD4)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{weakHashAlgorithm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Weak hash algorithm",cwe:"CWE-327",description:"Use of weak hash algorithm: {{algorithm}}. {{algorithm}} is cryptographically broken and unsuitable for security purposes.",severity:"CRITICAL",fix:'Replace with {{replacement}}: crypto.createHash("{{replacement}}").update(data). If this hash is an identifier rather than a security control \u2014 an EVALSHA key, an ETag, a cache key \u2014 store it under one of the nonCryptographicNames instead.',documentationLink:"https://owasp.org/www-community/vulnerabilities/Weak_Cryptography"}),useSha256:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-256",description:"Replace with SHA-256 for secure hashing",severity:"LOW",fix:'crypto.createHash("sha256").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"}),useSha512:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-512",description:"Replace with SHA-512 for stronger hashing",severity:"LOW",fix:'crypto.createHash("sha512").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"}),useSha3:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use SHA-3",description:"Replace with SHA-3 for latest standard",severity:"LOW",fix:'crypto.createHash("sha3-256").update(data)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatehashmethod-options"})},schema:[{type:"object",properties:{additionalWeakAlgorithms:{type:"array",items:{type:"string"},default:[],description:"Additional weak algorithms to detect"},allowInTests:{type:"boolean",default:false,description:"Allow weak hashes in test files"},nonCryptographicNames:{type:"array",items:{type:"string"},default:DEFAULT_NON_CRYPTOGRAPHIC_NAMES,description:"Assignment target names that mark a hash as an identifier rather than a security control"},securityUseNames:{type:"array",items:{type:"string"},default:DEFAULT_SECURITY_USE_NAMES,description:"Names that mark a hash as a security control (whole-word matched)"},reportUnclassifiedHashes:{type:"boolean",default:false,description:"Report weak hashes whose purpose cannot be determined. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{additionalWeakAlgorithms:[],allowInTests:false,nonCryptographicNames:DEFAULT_NON_CRYPTOGRAPHIC_NAMES}],create(context,[options={}]){const{additionalWeakAlgorithms=[],allowInTests=false,nonCryptographicNames=DEFAULT_NON_CRYPTOGRAPHIC_NAMES,securityUseNames=DEFAULT_SECURITY_USE_NAMES,reportUnclassifiedHashes=false}=options;const isSecurityUse=(0,names_1.makeNameTest)(securityUseNames);const filename=context.filename;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);const nonCryptoNames=new Set(nonCryptographicNames.map(normalizeName));function isNonCryptographicUse(node){const name=assignedName(node);return name!==null&&nonCryptoNames.has(normalizeName(name))}function hasSecurityUse(node){const stored=assignedName(node);if(stored!==null&&isSecurityUse(stored))return true;for(const argument of hashInputNames(node)){if(isSecurityUse(argument))return true}const enclosing=enclosingFunctionName(node);return enclosing!==null&&isSecurityUse(enclosing)}function checkCallExpression(node){if(isTestFile)return;if(isNonCryptographicUse(node))return;if(!reportUnclassifiedHashes&&!hasSecurityUse(node))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==="createHash"){checkHashArgument(node)}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="createHash"){checkHashArgument(node)}if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){const funcName=node.callee.name.toLowerCase();if(funcName==="sha1"||funcName==="md5"||funcName==="md4"){const weakPattern=findWeakHash(funcName,additionalWeakAlgorithms);context.report({node,messageId:"weakHashAlgorithm",data:{algorithm:weakPattern.name,replacement:weakPattern.replacement},suggest:[{messageId:"useSha256",fix:fixer=>{if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return fixer.replaceText(node.callee,"sha256")}return null}}]})}}}function checkHashArgument(node){for(const arg of node.arguments){if(arg.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof arg.value==="string"){const weakPattern=findWeakHash(arg.value,additionalWeakAlgorithms);if(weakPattern){context.report({node:arg,messageId:"weakHashAlgorithm",data:{algorithm:weakPattern.name,replacement:weakPattern.replacement},suggest:[{messageId:"useSha256",fix:fixer=>fixer.replaceText(arg,`"sha256"`)},{messageId:"useSha512",fix:fixer=>fixer.replaceText(arg,`"sha512"`)},{messageId:"useSha3",fix:fixer=>fixer.replaceText(arg,`"sha3-256"`)}]})}}}}return{CallExpression:checkCallExpression}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noZipSlip=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const containsPathTraversal=pathText=>{return/\.\.\//.test(pathText)||/\.\.\\/.test(pathText)||pathText.startsWith("..")||/\/\.\./.test(pathText)};const isDangerousDestination=destText=>{if(destText.startsWith("/tmp")||destText.includes("os.tmpdir")||destText.includes("TMPDIR")){return false}return destText.includes("/var")||destText.includes("/usr")||destText.includes("/etc")||destText.includes("/root")||destText.includes("/home")||destText.includes("C:\\Windows")||destText.includes("C:\\Program Files")||destText.includes("C:\\Users")};exports.noZipSlip=(0,eslint_devkit_1.createRule)({name:"no-zip-slip",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-zip-slip.md",description:"Detects zip slip/archive extraction vulnerabilities",cwe:"CWE-22"},hasSuggestions:true,messages:{zipSlipVulnerability:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Zip Slip Vulnerability",cwe:"CWE-22",description:"Archive extraction vulnerable to path traversal",severity:"{{severity}}",fix:"{{safeAlternative}}",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),unsafeArchiveExtraction:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unsafe Archive Extraction",cwe:"CWE-22",description:"Archive extraction without path validation",severity:"HIGH",fix:"Use safe extraction libraries or validate all paths",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),pathTraversalInArchive:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Path Traversal in Archive",cwe:"CWE-22",description:"Archive contains path traversal sequences",severity:"CRITICAL",fix:"Reject archives with path traversal or sanitize paths",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),unvalidatedArchivePath:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unvalidated Archive Path",cwe:"CWE-22",description:"Archive entry path used without validation",severity:"HIGH",fix:"Validate paths before extraction",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),dangerousArchiveDestination:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Dangerous Archive Destination",cwe:"CWE-22",description:"Archive extracted to sensitive location",severity:"MEDIUM",fix:"Extract to safe temporary directory",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),useSafeArchiveExtraction:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Use Safe Archive Extraction",description:"Use libraries with built-in path validation",severity:"LOW",fix:"Use yauzl, safe-archive-extract, or similar safe libraries",documentationLink:"https://www.npmjs.com/package/yauzl"}),validateArchivePaths:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Validate Archive Paths",description:"Validate all archive entry paths",severity:"LOW",fix:"Check paths don't contain ../ and are within destination directory",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),sanitizeArchiveNames:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Sanitize Archive Names",description:"Sanitize archive entry names",severity:"LOW",fix:"Use path.basename() or custom sanitization",documentationLink:"https://nodejs.org/api/path.html#pathbasenamepath-ext"}),strategyPathValidation:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Path Validation Strategy",description:"Validate paths before any file operations",severity:"LOW",fix:"Check path.startsWith(destination) and no ../ sequences",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),strategySafeLibraries:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Safe Libraries Strategy",description:"Use archive libraries with built-in safety",severity:"LOW",fix:"Use yauzl, adm-zip with validation, or safe-archive-extract",documentationLink:"https://www.npmjs.com/package/safe-archive-extract"}),strategySandboxing:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Sandboxing Strategy",description:"Extract archives in sandboxed environment",severity:"LOW",fix:"Use temporary directories and restrict permissions",documentationLink:"https://nodejs.org/api/fs.html#fsopentempdirprefix-options-callback"})},schema:[{type:"object",properties:{archiveFunctions:{type:"array",items:{type:"string"},default:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"]},pathValidationFunctions:{type:"array",items:{type:"string"},default:["validatePath","sanitizePath","checkPath","safePath"]},safeLibraries:{type:"array",items:{type:"string"},default:["yauzl","safe-archive-extract","tar-stream","unzipper"]}},additionalProperties:false}]},defaultOptions:[{archiveFunctions:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions:["validatePath","sanitizePath","checkPath","safePath"],safeLibraries:["yauzl","safe-archive-extract","tar-stream","unzipper"]}],create(context){const options=context.options[0]||{};const{archiveFunctions=["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions=["validatePath","sanitizePath","checkPath","safePath"],safeLibraries=["yauzl","safe-archive-extract","tar-stream","unzipper"]}=options;const filename=context.filename;const isArchiveExtraction=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&archiveFunctions.includes(callee.property.name)){return true}if(callee.type==="Identifier"&&archiveFunctions.includes(callee.name)){return true}return false};const isPathValidated=pathNode=>{let current=pathNode;while(current){if(current.type==="CallExpression"&&current.callee.type==="Identifier"&&pathValidationFunctions.includes(current.callee.name)){return true}if(current.type==="CallExpression"&&current.callee.type==="MemberExpression"&&current.callee.object.type==="Identifier"&&current.callee.object.name==="path"&&current.callee.property.type==="Identifier"&&current.callee.property.name==="basename"){return true}if(current.type==="IfStatement"){const test=current.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="startsWith"){return true}if(test.type==="UnaryExpression"&&test.operator==="!"&&test.argument.type==="CallExpression"&&test.argument.callee.type==="MemberExpression"&&test.argument.callee.property.type==="Identifier"&&test.argument.callee.property.name==="startsWith"){return true}if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){return true}}current=current.parent}return false};const isSafeLibrary=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.object.type==="Identifier"&&safeLibraries.includes(callee.object.name)){return true}if(callee.type==="Identifier"){const name=callee.name.toLowerCase();if(name==="extract"||name==="unzipper"||safeLibraries.some(lib=>name.includes(lib.toLowerCase()))){return true}}return false};return{CallExpression(node){if(isArchiveExtraction(node)&&!isSafeLibrary(node)){const sourceCode=context.sourceCode;let hasSafeAnnotation=false;const allComments=sourceCode.getAllComments();for(const comment of allComments){if(comment.type==="Block"&&comment.value.includes("@safe")){hasSafeAnnotation=true;break}}if(hasSafeAnnotation){return}const args=node.arguments;let destArg;if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(["extractAllTo","unzip"].includes(methodName)){destArg=args[0]}else{destArg=args.length>=2?args[1]:void 0}}else{destArg=args.length>=2?args[1]:void 0}const destText=destArg&&destArg.type==="Literal"&&typeof destArg.value==="string"?destArg.value:"";const isDestDangerous=isDangerousDestination(destText);const isMethodCall=node.callee.type==="MemberExpression";if(isMethodCall){const isSafeRelativePath=destText.startsWith("./")||destText.startsWith("../");if(!isSafeRelativePath){context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)},suggest:[{messageId:"useSafeArchiveExtraction",fix:()=>null}]})}if(isDestDangerous&&destArg){context.report({node:destArg,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}else{if(isDestDangerous){context.report({node,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}else{context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)},suggest:[{messageId:"useSafeArchiveExtraction",fix:()=>null}]})}}}const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&["join","resolve","relative","normalize"].includes(callee.property.name)){const args=node.arguments;for(const arg of args){if(arg.type==="MemberExpression"&&arg.property.type==="Identifier"&&["name","path","fileName","entryName","relativePath","filename","pathname"].includes(arg.property.name)){if(!isPathValidated(arg)){context.report({node:arg,messageId:"unvalidatedArchivePath",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}}}},Literal(node){if(typeof node.value!=="string"){return}const text=node.value;if((text.includes("/")||text.includes("\\"))&&containsPathTraversal(text)){let current=node;let isArchiveContext=false;while(current){if(current.type==="CallExpression"&&isArchiveExtraction(current)){isArchiveContext=true;break}if(current.type==="VariableDeclarator"&&current.id.type==="Identifier"&&(current.id.name.includes("archive")||current.id.name.includes("zip")||current.id.name.includes("tar")||current.id.name.includes("path")||current.id.name.includes("file")||current.id.name.includes("entry"))){isArchiveContext=true;break}current=current.parent}const parent=node.parent;if(parent&&parent.type==="VariableDeclarator"&&parent.id.type==="Identifier"){const varName=parent.id.name.toLowerCase();if(varName.includes("archive")||varName.includes("zip")||varName.includes("tar")||varName.includes("path")||varName.includes("file")||varName.includes("extract")||varName.includes("entry")){isArchiveContext=true}}if(isArchiveContext){context.report({node,messageId:"pathTraversalInArchive",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}},VariableDeclarator(node){if(!node.init||node.id.type!=="Identifier"){return}const varName=node.id.name.toLowerCase();if(varName.includes("entry")||varName.includes("file")||varName.includes("path")){if(node.init.type==="MemberExpression"&&node.init.property.type==="Identifier"&&["name","path"].includes(node.init.property.name)){}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noZipSlip=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const DEFAULT_ARCHIVE_MODULES=["adm-zip","unzipper","yauzl","yazl","tar","tar-fs","tar-stream","extract-zip","node-stream-zip","jszip","archiver","decompress","unzip-stream","zip-stream","gunzip-maybe","7zip-min","node-7z"];const containsPathTraversal=pathText=>{return/\.\.\//.test(pathText)||/\.\.\\/.test(pathText)||pathText.startsWith("..")||/\/\.\./.test(pathText)};const isDangerousDestination=destText=>{if(destText.startsWith("/tmp")||destText.includes("os.tmpdir")||destText.includes("TMPDIR")){return false}return destText.includes("/var")||destText.includes("/usr")||destText.includes("/etc")||destText.includes("/root")||destText.includes("/home")||destText.includes("C:\\Windows")||destText.includes("C:\\Program Files")||destText.includes("C:\\Users")};exports.noZipSlip=(0,eslint_devkit_1.createRule)({name:"no-zip-slip",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-zip-slip.md",description:"Detects zip slip/archive extraction vulnerabilities",cwe:"CWE-22"},hasSuggestions:true,messages:{zipSlipVulnerability:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Zip Slip Vulnerability",cwe:"CWE-22",description:"Archive extraction vulnerable to path traversal",severity:"{{severity}}",fix:"{{safeAlternative}}",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),unsafeArchiveExtraction:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unsafe Archive Extraction",cwe:"CWE-22",description:"Archive extraction without path validation",severity:"HIGH",fix:"Use safe extraction libraries or validate all paths",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),pathTraversalInArchive:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Path Traversal in Archive",cwe:"CWE-22",description:"Archive contains path traversal sequences",severity:"CRITICAL",fix:"Reject archives with path traversal or sanitize paths",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),unvalidatedArchivePath:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Unvalidated Archive Path",cwe:"CWE-22",description:"Archive entry path used without validation",severity:"HIGH",fix:"Validate paths before extraction",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),dangerousArchiveDestination:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.SECURITY,issueName:"Dangerous Archive Destination",cwe:"CWE-22",description:"Archive extracted to sensitive location",severity:"MEDIUM",fix:"Extract to safe temporary directory",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),useSafeArchiveExtraction:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Use Safe Archive Extraction",description:"Use libraries with built-in path validation",severity:"LOW",fix:"Use yauzl, safe-archive-extract, or similar safe libraries",documentationLink:"https://www.npmjs.com/package/yauzl"}),validateArchivePaths:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Validate Archive Paths",description:"Validate all archive entry paths",severity:"LOW",fix:"Check paths don't contain ../ and are within destination directory",documentationLink:"https://snyk.io/research/zip-slip-vulnerability"}),sanitizeArchiveNames:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.INFO,issueName:"Sanitize Archive Names",description:"Sanitize archive entry names",severity:"LOW",fix:"Use path.basename() or custom sanitization",documentationLink:"https://nodejs.org/api/path.html#pathbasenamepath-ext"}),strategyPathValidation:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Path Validation Strategy",description:"Validate paths before any file operations",severity:"LOW",fix:"Check path.startsWith(destination) and no ../ sequences",documentationLink:"https://cwe.mitre.org/data/definitions/22.html"}),strategySafeLibraries:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Safe Libraries Strategy",description:"Use archive libraries with built-in safety",severity:"LOW",fix:"Use yauzl, adm-zip with validation, or safe-archive-extract",documentationLink:"https://www.npmjs.com/package/safe-archive-extract"}),strategySandboxing:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.STRATEGY,issueName:"Sandboxing Strategy",description:"Extract archives in sandboxed environment",severity:"LOW",fix:"Use temporary directories and restrict permissions",documentationLink:"https://nodejs.org/api/fs.html#fsopentempdirprefix-options-callback"})},schema:[{type:"object",properties:{archiveFunctions:{type:"array",items:{type:"string"},default:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"]},pathValidationFunctions:{type:"array",items:{type:"string"},default:["validatePath","sanitizePath","checkPath","safePath"]},safeLibraries:{type:"array",items:{type:"string"},default:["yauzl","safe-archive-extract","tar-stream","unzipper"]},archiveModules:{type:"array",items:{type:"string"},default:DEFAULT_ARCHIVE_MODULES,description:"Module specifiers that mean this file works with archives"},reportWithoutArchiveContext:{type:"boolean",default:false,description:"Report entry-name and traversal shapes in files with no archive. Restores the pre-inversion behaviour."}},additionalProperties:false}]},defaultOptions:[{archiveFunctions:["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions:["validatePath","sanitizePath","checkPath","safePath"],safeLibraries:["yauzl","safe-archive-extract","tar-stream","unzipper"]}],create(context){const options=context.options[0]||{};const{archiveFunctions=["extract","extractAll","extractAllTo","unzip","untar","extractArchive"],pathValidationFunctions=["validatePath","sanitizePath","checkPath","safePath"],safeLibraries=["yauzl","safe-archive-extract","tar-stream","unzipper"],archiveModules=DEFAULT_ARCHIVE_MODULES,reportWithoutArchiveContext=false}=options;const filename=context.filename;const archiveModuleSet=new Set(archiveModules.map(name=>name.toLowerCase()));let hasArchiveContext=reportWithoutArchiveContext;const isArchiveModule=specifier=>typeof specifier==="string"&&archiveModuleSet.has(specifier.toLowerCase());const ARCHIVE_NAME=/zip|tarball|archive|gzip|gunzip|untar|tarstream|\btar\b/i;const namesArchive=node=>{if(node.type==="Identifier")return ARCHIVE_NAME.test(node.name);if(node.type==="MemberExpression"){return namesArchive(node.object)||node.property.type==="Identifier"&&ARCHIVE_NAME.test(node.property.name)}return false};const pending=[];const isArchiveExtraction=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&archiveFunctions.includes(callee.property.name)){return true}if(callee.type==="Identifier"&&archiveFunctions.includes(callee.name)){return true}return false};const isPathValidated=pathNode=>{let current=pathNode;while(current){if(current.type==="CallExpression"&&current.callee.type==="Identifier"&&pathValidationFunctions.includes(current.callee.name)){return true}if(current.type==="CallExpression"&&current.callee.type==="MemberExpression"&&current.callee.object.type==="Identifier"&&current.callee.object.name==="path"&&current.callee.property.type==="Identifier"&&current.callee.property.name==="basename"){return true}if(current.type==="IfStatement"){const test=current.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="startsWith"){return true}if(test.type==="UnaryExpression"&&test.operator==="!"&&test.argument.type==="CallExpression"&&test.argument.callee.type==="MemberExpression"&&test.argument.callee.property.type==="Identifier"&&test.argument.callee.property.name==="startsWith"){return true}if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){return true}}current=current.parent}return false};const isSafeLibrary=node=>{const callee=node.callee;if(callee.type==="MemberExpression"&&callee.object.type==="Identifier"&&safeLibraries.includes(callee.object.name)){return true}if(callee.type==="Identifier"){const name=callee.name.toLowerCase();if(name==="extract"||name==="unzipper"||safeLibraries.some(lib=>name.includes(lib.toLowerCase()))){return true}}return false};return{ImportDeclaration(node){if(isArchiveModule(node.source.value))hasArchiveContext=true},NewExpression(node){if(namesArchive(node.callee))hasArchiveContext=true},"Program:exit"(){for(const report of pending)report()},CallExpression(node){if(node.callee.type==="Identifier"&&node.callee.name==="require"&&node.arguments[0]?.type==="Literal"&&isArchiveModule(node.arguments[0].value)){hasArchiveContext=true}if(isArchiveExtraction(node)||namesArchive(node.callee))hasArchiveContext=true;if(isArchiveExtraction(node)&&!isSafeLibrary(node)){const sourceCode=context.sourceCode;let hasSafeAnnotation=false;const allComments=sourceCode.getAllComments();for(const comment of allComments){if(comment.type==="Block"&&comment.value.includes("@safe")){hasSafeAnnotation=true;break}}if(hasSafeAnnotation){return}const args=node.arguments;let destArg;if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(["extractAllTo","unzip"].includes(methodName)){destArg=args[0]}else{destArg=args.length>=2?args[1]:void 0}}else{destArg=args.length>=2?args[1]:void 0}const destText=destArg&&destArg.type==="Literal"&&typeof destArg.value==="string"?destArg.value:"";const isDestDangerous=isDangerousDestination(destText);const isMethodCall=node.callee.type==="MemberExpression";if(isMethodCall){const isSafeRelativePath=destText.startsWith("./")||destText.startsWith("../");if(!isSafeRelativePath){context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)},suggest:[{messageId:"useSafeArchiveExtraction",fix:()=>null}]})}if(isDestDangerous&&destArg){context.report({node:destArg,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}}else{if(isDestDangerous){context.report({node,messageId:"dangerousArchiveDestination",data:{filePath:filename,line:String(node.loc?.start.line??0)}})}else{context.report({node,messageId:"unsafeArchiveExtraction",data:{filePath:filename,line:String(node.loc?.start.line??0)},suggest:[{messageId:"useSafeArchiveExtraction",fix:()=>null}]})}}}const callee=node.callee;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&["join","resolve","relative","normalize"].includes(callee.property.name)){const args=node.arguments;for(const arg of args){if(arg.type==="MemberExpression"&&arg.property.type==="Identifier"&&["name","path","fileName","entryName","relativePath","filename","pathname"].includes(arg.property.name)){if(!isPathValidated(arg)){pending.push(()=>{if(!hasArchiveContext)return;context.report({node:arg,messageId:"unvalidatedArchivePath",data:{filePath:filename,line:String(node.loc?.start.line??0)}})})}}}}},Literal(node){if(typeof node.value!=="string"){return}const text=node.value;if(!(text.includes("/")||text.includes("\\"))||!containsPathTraversal(text)){return}let current=node;while(current){if(current.type==="CallExpression"&&isArchiveExtraction(current)){context.report({node,messageId:"pathTraversalInArchive",data:{filePath:filename,line:String(node.loc?.start.line??0)}});return}current=current.parent}},VariableDeclarator(node){if(!node.init||node.id.type!=="Identifier"){return}const varName=node.id.name.toLowerCase();if(varName.includes("entry")||varName.includes("file")||varName.includes("path")){if(node.init.type==="MemberExpression"&&node.init.property.type==="Identifier"&&["name","path"].includes(node.init.property.name)){}}}}}});
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAeadTagVerification=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const AEAD_SUFFIX=/-(gcm|ccm|ocb|poly1305)$/;const STREAM_METHODS=new Set(["pipe","write","end","setEncoding"]);exports.requireAeadTagVerification=(0,eslint_devkit_1.createRule)({name:"require-aead-tag-verification",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-aead-tag-verification.md",description:"Require AEAD decryption to verify the authentication tag (setAuthTag + final)",cwe:"CWE-327",cvss:7.5},hasSuggestions:false,messages:{missingAuthTag:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"AEAD decryption without setAuthTag",cwe:"CWE-327",description:"An AEAD decipher (GCM/CCM/OCB/ChaCha20-Poly1305) is created but setAuthTag() is never called, so the authentication tag is never checked. Forged or tampered ciphertext decrypts as if it were authentic.",severity:"HIGH",fix:"Call decipher.setAuthTag(tag) with the tag produced at encryption time, then decipher.final()",documentationLink:"https://nodejs.org/api/crypto.html#decipher_setauthtagbuffer"}),missingFinal:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"AEAD decryption never calls final()",cwe:"CWE-327",description:"setAuthTag() was called but decipher.final() never is. final() is the call that compares the tag and throws on mismatch \u2014 without it the tag is loaded and then ignored, so update() returns unauthenticated plaintext.",severity:"HIGH",fix:"Append decipher.final() to the decryption and let it throw on a tag mismatch",documentationLink:"https://nodejs.org/api/crypto.html#decipherfinaloutputencoding"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow unverified AEAD decryption in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(context.filename);function isCreateDecipheriv(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return callee.property.name==="createDecipheriv"}return callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.name==="createDecipheriv"}function isAeadAlgorithm(argument){return argument!==void 0&&argument.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof argument.value==="string"&&AEAD_SUFFIX.test(argument.value.toLowerCase())}function collectMethods(variable){const methods=new Set;for(const reference of variable.references){const identifier=reference.identifier;const parent=identifier.parent;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator)continue;if(parent.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&parent.object===identifier&&parent.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&!parent.computed){methods.add(parent.property.name);continue}return null}return methods}function checkVariableDeclarator(node){if(isTestFile)return;if(node.id.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return;const init=node.init;if(!init||init.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return;if(!isCreateDecipheriv(init.callee))return;if(!isAeadAlgorithm(init.arguments[0]))return;const[variable]=context.sourceCode.getDeclaredVariables(node);const methods=collectMethods(variable);if(methods===null)return;if(!methods.has("setAuthTag")){context.report({node:init,messageId:"missingAuthTag"});return}const drivenAsStream=[...methods].some(name=>STREAM_METHODS.has(name));if(!methods.has("final")&&!drivenAsStream){context.report({node:init,messageId:"missingFinal"})}}return{VariableDeclarator:checkVariableDeclarator}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireDependencyIntegrity=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireDependencyIntegrity=(0,eslint_devkit_1.createRule)({name:"require-dependency-integrity",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-dependency-integrity.md",description:"Require SRI (Subresource Integrity) for CDN resources",cwe:"CWE-494",cvss:8.1},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing SRI",cwe:"CWE-494",description:"External resource loaded without integrity hash - supply chain risk",severity:"HIGH",fix:'Add integrity="sha384-..." and crossorigin="anonymous" attributes',documentationLink:"https://cwe.mitre.org/data/definitions/494.html"})},schema:[]},defaultOptions:[],create(context){function report(node){context.report({node,messageId:"violationDetected"})}return{Literal(node){if(typeof node.value!=="string")return;const value=node.value.toLowerCase();if(value.includes("<script")&&value.includes("src=")||value.includes("<link")&&value.includes("href=")){if(value.includes("cdn.")||value.includes("cdnjs.")||value.includes("unpkg.")||value.includes("jsdelivr.")){if(!value.includes("integrity=")){report(node)}}}},TemplateLiteral(node){const text=context.sourceCode.getText(node).toLowerCase();if(text.includes("<script")&&text.includes("src=")||text.includes("<link")&&text.includes("href=")){if(text.includes("cdn.")||text.includes("cdnjs.")||text.includes("unpkg.")||text.includes("jsdelivr.")){if(!text.includes("integrity=")){report(node)}}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireDependencyIntegrity=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const RESOURCE_TAG=/<(script|link)\b[^>]*>/gi;const CDN_HOSTS=["cdn.","cdnjs.","unpkg.","jsdelivr."];function hasUnprotectedCdnTag(text){for(const match of text.matchAll(RESOURCE_TAG)){const tag=match[0].toLowerCase();const urlAttribute=match[1].toLowerCase()==="script"?"src=":"href=";if(!tag.includes(urlAttribute))continue;if(!CDN_HOSTS.some(host=>tag.includes(host)))continue;if(tag.includes("integrity="))continue;return true}return false}exports.requireDependencyIntegrity=(0,eslint_devkit_1.createRule)({name:"require-dependency-integrity",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-dependency-integrity.md",description:"Require SRI (Subresource Integrity) for CDN resources",cwe:"CWE-494",cvss:8.1},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing SRI",cwe:"CWE-494",description:"External resource loaded without integrity hash - supply chain risk",severity:"HIGH",fix:'Add integrity="sha384-..." and crossorigin="anonymous" attributes',documentationLink:"https://cwe.mitre.org/data/definitions/494.html"})},schema:[]},defaultOptions:[],create(context){function report(node){context.report({node,messageId:"violationDetected"})}return{Literal(node){if(typeof node.value!=="string")return;if(hasUnprotectedCdnTag(node.value))report(node)},TemplateLiteral(node){if(hasUnprotectedCdnTag(context.sourceCode.getText(node)))report(node)}}}});
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireStreamErrorHandler=void 0;exports.calleeMethodName=calleeMethodName;exports.constructsStream=constructsStream;const eslint_devkit_1=require("@interlace/eslint-devkit");const STREAM_CONSTRUCTORS=new Set(["createReadStream","createWriteStream","createGzip","createGunzip","createDeflate","createInflate","createBrotliCompress","createBrotliDecompress"]);const LISTENER_METHODS=new Set(["on","once","addListener","prependListener","prependOnceListener"]);function calleeMethodName(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return callee.name;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&!callee.computed&&callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return callee.property.name}return void 0}function constructsStream(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const name=calleeMethodName(node.callee);return name!==void 0&&STREAM_CONSTRUCTORS.has(name)}exports.requireStreamErrorHandler=(0,eslint_devkit_1.createRule)({name:"require-stream-error-handler",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/require-stream-error-handler.md",description:"Require an 'error' listener on streams passed to .pipe(), which does not forward errors",cwe:"CWE-248",cvss:7.5},hasSuggestions:true,messages:{unhandledStreamError:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unhandled stream error",cwe:"CWE-248",description:".pipe() forwards data but not errors. A stream that emits 'error' with no listener throws inside the EventEmitter, which is an uncaught exception \u2014 the process exits. A single request for a missing or unreadable file is enough to stop the server.",severity:"HIGH",fix:"Attach stream.on('error', handler) before piping, or use pipeline(), which destroys every stream and reports the failure.",documentationLink:"https://cwe.mitre.org/data/definitions/248.html"}),attachErrorListener:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Attach an 'error' listener",description:"Name the stream and handle 'error' before piping",severity:"LOW",fix:"const s = fs.createReadStream(p); s.on('error', next); s.pipe(res);",documentationLink:"https://nodejs.org/api/stream.html#readablepipedestination-options"}),usePipeline:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use pipeline()",description:"pipeline() propagates errors and destroys every stream",severity:"LOW",fix:"await pipeline(fs.createReadStream(p), res) // 'stream/promises'",documentationLink:"https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-callback"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:true,description:"Allow unhandled stream errors in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:true}],create(context,[options={}]){const{allowInTests=true}=options;if(allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(context.filename)){return{}}const handled=new Set;const streamBindings=new Set;const pending=[];function unhandledStream(node){if(constructsStream(node))return node;if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;if(!streamBindings.has(node.name))return null;if(handled.has(node.name))return null;return node}return{VariableDeclarator(node){if(node.init!==null&&node.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&constructsStream(node.init)){streamBindings.add(node.id.name)}},CallExpression(node){const callee=node.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression||callee.computed||callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier){return}const method=callee.property.name;if(LISTENER_METHODS.has(method)){const event=node.arguments[0];if(event?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&event.value==="error"&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){handled.add(callee.object.name)}return}if(method!=="pipe")return;pending.push({node,offender:node})},"Program:exit"(){for(const{node}of pending){const callee=node.callee;const source=unhandledStream(callee.object);const destinationArg=node.arguments[0];const destination=destinationArg===void 0||destinationArg.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement?null:unhandledStream(destinationArg);const offender=source??destination;if(offender===null)continue;context.report({node:offender,messageId:"unhandledStreamError",suggest:[{messageId:"attachErrorListener",fix:()=>null},{messageId:"usePipeline",fix:()=>null}]})}}}}});