UNPKG

@hclsoftware/secagent

Version:

IAST agent

257 lines (227 loc) 10.5 kB
//IASTIGNORE /* * **************************************************** * Licensed Materials - Property of HCL. * (c) Copyright HCL Technologies Ltd. 2017, 2025. * Note to U.S. Government Users *Restricted Rights. * **************************************************** */ 'use strict' const Utils = require("../Utils/Utils") const ContextType = require("./ContextType.js") const VerificationTracker = require("../Verification/VerificationTracker") const TaintTracker = require("../TaintTracker") const IastLogger = require("../Logger/IastLogger") const logger = IastLogger.eventLog const TaskType = require("./TaskType") const Vulnerability = require('../Vulnerability') const SinkTaskInfo = require("./SinkInfo/SinkTaskInfo") const {SinkInfoStringGenerator} = require("./SinkInfo/SinkInfoStringGenerator") const CONTEXT_QUOTE_CHARS = ["\'", "\""] const UNKNOWN_CLOSER_CHARS = ["\'", "\"", "\)", "\]", "\}"] class SinkTask { constructor (source, vulnerability, stack, parameters) { // originally extends ThreadedTask.. this.taintedObjectFlow = source this.vulnerability = vulnerability this.stack = stack this.parameters = parameters } // delayed sinkTrigger performAction () { // TODO: algorithms here console.origLog(`performAction called, vulnerability: ${this.vulnerability}`) this.taintedObjectFlow.sinkTrigger(this.vulnerability) } performContextsAnalysis (context, parameterUnderTest){ let sinkResults = null let vulnerabilitySanitized = true for(const ct of context.getContextsTypes()){ if(ct.getContextTypesStackBased().origArrayIncludes(ContextType["NO_REFLECTION"].contextTypeName)){ // when no reflection detected, we report only if there is are no sanitizers (tasks) vulnerabilitySanitized = !parameterUnderTest.emptyTaskList(); } else if (ct.getContextTypesStackBased().origArrayIncludes(ContextType["MULTIPLE_REFLECTION"].contextTypeName)){ // when multiple reflections detected, we report only if there is are no sanitizers (tasks) vulnerabilitySanitized = !parameterUnderTest.emptyTaskList(); } else{ // check control // vulnerabilitySanitized = true // const signature = this.getTasksInfo() // const signature = VerificationTracker.getSignature(this.getTasksInfo(), this.vulnerability, ct ,) const signature = this.getTasksInfoReadable(ct) const verified = VerificationTracker.getVerifiedState(this.getTasksInfo(), this.vulnerability, ct , signature) if (verified == null) { logger.debug(`Checking vulnerability ${this.vulnerability} for signature ${signature}`) sinkResults = this.runContextAwareAnalysis(ct, parameterUnderTest); vulnerabilitySanitized = sinkResults == null if(vulnerabilitySanitized){ logger.debug(`Signature ${signature} is verified as a sanitizer`) sinkResults = null } else { logger.debug(`Signature ${signature} is verified as vulnerable`) } VerificationTracker.setSignatureAsVerified(this.getTasksInfo(), this.vulnerability, vulnerabilitySanitized, sinkResults, ct, signature) } else { if(verified.verifiedOk === true){ logger.debug(`Signature ${signature} has already been verified as a safe sanitizer`) }else{ logger.debug(`Signature ${signature} has already been verified as a vulnerable sanitizer`) } vulnerabilitySanitized = verified.verifiedOk sinkResults = verified.sinkInfo } } } if(vulnerabilitySanitized) { this.taintedObjectFlow.sanitize(this.vulnerability) } else { if(this.vulnerability !== null && this.vulnerability.origStartsWith('Injection.OS')) this.vulnerability = Vulnerability.COMMAND_INJECTION TaintTracker.sinkTaskTrigger(this.taintedObjectFlow, this.vulnerability, sinkResults != null ? new SinkInfoStringGenerator(sinkResults) : null, true, this.stack, this.parameters) } return !vulnerabilitySanitized } runContextAwareAnalysis (contextType, parameterUnderTest){ const exploitList = this.getPayloadsForContextStack(contextType) for(const exploit of exploitList){ const processingResult = parameterUnderTest.getMutatedProcessedParam(exploit) const processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(this.containsBlacklistedChars(processedLegitimateExploitForCodePath, contextType)){ return new SinkTaskInfo(exploit, contextType.getContextTypesStackBased(), exploitList, this.vulnerability) } } } return null } getPayloadsForContextStack (ctxStack){ let payLoadsForCtx = [] if(ctxStack.getContextTypesStackBased().origArrayIncludes(ContextType["UNKNOWN"].contextTypeName)){ payLoadsForCtx.concat(UNKNOWN_CLOSER_CHARS) } else { for ( let ctx of ctxStack.getContextTypesStackBased()){ if(ContextType[ctx] === ContextType["UNQUOTED"]) continue payLoadsForCtx.push(ContextType[ctx].getCtxCloser) } } return this.updatePayloadsWithComments(payLoadsForCtx) } //abstract containsBlacklistedChars (possiblySanitizedPayload) {} // abstract getCommentChars() {} static containsUnescapedChars(inputString , escapableCharsObject, escapeByDoubleChar, escapeCharacter){ let escapableChars = Array.isArray(escapableCharsObject) ? escapableCharsObject : Object.keys(escapableCharsObject) if( inputString.length === 0 || escapableChars.length === 0){ return false } // new line character sequence is a special case if (escapableChars.origArrayIncludes('\n') && inputString.origStringIncludes("\n")) return true; for (let q of escapableChars){ // escape char is a special case, can be escapes(\\) or as escape char(ex: \") if (q.charAt(0) === escapeCharacter) { let counter = 0 for(let i = 0; i < inputString.length; i++){ if(inputString.charAt(i) === escapeCharacter){ i++ counter++ while(i < inputString.length && inputString.charAt(i) === escapeCharacter){ i++ counter++ } if (i < inputString.length) { if ((counter % 2) === 0 && !escapableChars.origArrayIncludes((inputString.charAt(i)))) counter = 0; else if ((counter % 2) !== 0 && escapableChars.origArrayIncludes(inputString.charAt(i))) counter = 0; else return true; } else{ //string ends with escape character if((counter % 2) !== 0) return true; } } } } else { q = Array.isArray(escapableCharsObject) ? q : escapableCharsObject[q] let nChars = (inputString.origMatch(RegExp(q,"g")) || []).length let escapedChar = (inputString.origMatch(RegExp("\\" + escapeCharacter + q, "g")) || []).length let doubleChar = escapeByDoubleChar ? (inputString.origMatch(RegExp(q + q,"g")) || []).length : 0; if ((nChars - escapedChar - 2 * doubleChar) !== 0) return true; } } return false; } updatePayloadsWithComments(exploitStack){ var output = [] if (exploitStack.length === 0) return output.concat(this.getCommentChars()) else { for (const payload of exploitStack) { for (const cmntChar of this.getCommentChars()) { output.push(payload + cmntChar); } } } return output; } static stripMatchingQuoteEdgeChars(possiblySanitizedPayload){ let workString = possiblySanitizedPayload let modifiedWorkString = possiblySanitizedPayload do { if (modifiedWorkString.length !== workString.length) { workString = modifiedWorkString; } for (const s of CONTEXT_QUOTE_CHARS){ const ss = s+s; if (workString.length > 3 & (workString.origStartsWith(ss) && workString.origEndsWith(ss)) && !workString.origEndsWith("\\"+ss)) { modifiedWorkString = workString.origSubstring(2, workString.length-2); } else if ((workString.origStartsWith(s) && workString.origEndsWith(s)) && !workString.origEndsWith("\\"+s)) { modifiedWorkString = workString.origSubstring(1, workString.length-1); } } } while (workString.length !== modifiedWorkString.length); return workString; } getTasksInfo () { const hash = Utils.createHashObject() for (const task of this.taintedObjectFlow.taskList) { hash.update(task.getSignature()) } return hash.produce() } getTasksInfoReadable (ct) { let tasksInfo = this.taskInfoDescription() +", TaskInfo :{" for(const task of this.taintedObjectFlow.taskList ) { tasksInfo = tasksInfo +" "+ task.taskInfoDescription() } tasksInfo = tasksInfo +"}, Context: "+ ct.getContextTypesStackBased().toString() return `[${tasksInfo}]` } traverseExploit (exploit) { // We need to go through the entire set of tasks and run them. // At the end of it all, we need to look for exploit chars. If we find them, the whole thing is vulnerable. // During traversal, if we find a case where the result of a verifier is different from the original result, the test is invalid. for (const task of this.taintedObjectFlow.taskList) { exploit = task.performAction(exploit) if (exploit == null || (task.taskType === TaskType.VALIDATION && !task.isVerified)) { // If we got a response that is different from the original response then we are not on the same path and the whole test is irrelevant. // In this case, we will return null. If all results are null, the entire test is invalid. return null } } return exploit } taskInfoDescription () { return `Source: "${this.taintedObjectFlow.entity.value}\", Vulnerability: ${this.vulnerability}` } } module.exports.SinkTask = SinkTask module.exports.CONTEXT_QUOTE_CHARS = CONTEXT_QUOTE_CHARS module.exports.UNKNOWN_CLOSER_CHARS = UNKNOWN_CLOSER_CHARS