@hclsoftware/secagent
Version:
IAST agent
257 lines (227 loc) • 10.5 kB
JavaScript
//IASTIGNORE
/*
* ****************************************************
* Licensed Materials - Property of HCL.
* (c) Copyright HCL Technologies Ltd. 2017, 2025.
* Note to U.S. Government Users *Restricted Rights.
* ****************************************************
*/
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