UNPKG

@hclsoftware/secagent

Version:

IAST agent

504 lines (449 loc) 22 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 Entity = require('./Entity') const StackInfo = require('./StackInfo') const VulnerabilityInfo = require('./VulnerabilityInfo') const Distributor = require('./Distributor/Distributor') const TaintedObjectData = require('./TaintedObjectData') const XssSinkTask = require('./Tasks/XssSinkTask') const PathTraversalSinkTask = require('./Tasks/PathTraversalSinkTask') const UnimplementedSinkTask = require('./Tasks/UnimplementedSinkTask') const TasksManager = require('./Tasks/TasksManager') const Vulnerability = require('./Vulnerability') const IastLogger = require('./Logger/IastLogger') const {ConfigInfo} = require('./ConfigFile/ConfigInfo') const Utils = require('./Utils/Utils') const IastProperties = require('./Hooks/IastProperties') const SqlSinkTask = require("./Tasks/SqlSinkTask"); const BashOsCommandingSinkTask = require("./Tasks/BashOsCommandingSinkTask") const CmdOsCommandingSinkTask = require("./Tasks/CmdOsCommandingSinkTask") const PowerShellOsCommandingSinkTask = require("./Tasks/PowerShellOsCommandingSinkTask") const AdditionalInfoObj = require("./AdditionalInfo") const IastDastSinkReporter = require("./IastDast/IastDastSinkReporter") const taintedObjectData = require("./TaintedObjectData"); const logger = IastLogger.eventLog const globals = require("./Globals") const {keys} = require("./AdditionalInfo"); const K8sPropagatorUtils = require("./Rules/Utils/K8sPropagatorUtils") const K8sSinkUtils = require("./Rules/Utils/K8sSinkUtils"); const HookRuleType = { SOURCE: 'source', SINK: 'sink', PROPAGATOR: 'propagator', EXPLOIT: 'exploit', SANITIZER: 'sanitizer', DESANITIZER: 'desanitizer', OPEN_SOURCE_LIBRARY: 'open source library', API_ENDPOINTS: 'API endpoints' } module.exports.registerObjectTaint = (obj, requestInfo, entityName, entityValue, entityType, params) => { if (obj == null) { return null } if (typeof obj == 'string') { obj = new String(obj) } const stackInfo = new StackInfo(HookRuleType.SOURCE, params, null, new global.origError()) const taintedData = taintedObjectData.taintedObjectDataWithFlow(requestInfo, entityName, entityValue, entityType) taintedData.addToStackList(stackInfo) module.exports.registerTaint(obj, taintedData) return obj } function ignoreCookieForVulnerability (entity, v) { if (entity != null && entity.type === Entity.EntityType.COOKIE) { return (v === Vulnerability.SECURE_COOKIE && ConfigInfo.ConfigInfo.ignoredNonSecureCookies.origArrayIncludes(entity.name)) || (v === Vulnerability.HTTPONLY_COOKIE && ConfigInfo.ConfigInfo.ignoredNonHttpOnlyCookies.origArrayIncludes(entity.name)) } return false } module.exports.isXssContentType = (contentType) => { if (contentType == null) { return true } const xssContentTypes = ['text/html', 'text/xml', 'application/javascript', 'application/x-javascript', 'text/javascript', 'application/xhtml+xml'] const lowerCaseContentType = contentType.toLowerCase() return xssContentTypes.some(type => lowerCaseContentType.origStringIncludes(type)) } function getTaintedData (source) { return source[IastProperties.property.TAINTED_DATA] } function doPropagateTaint(source, target, parameters) { if (source == null || target == null || !module.exports.isObjectTainted(source)) { return null } if (source !== target) { // if (typeof target === 'string') { // TODO: do it here? we must do it in string hook because we want the object back. or return the object? // target = new String(target) // } if (target[IastProperties.property.TAINTED_DATA] === undefined) { module.exports.registerTaint(target, new TaintedObjectData.TaintedObjectData()) // target[IastProperties.property.TAINTED_DATA] = new TaintedObjectData.TaintedObjectData() } } target[IastProperties.property.TAINTED_DATA].merge(source[IastProperties.property.TAINTED_DATA], parameters) StackInfo.replacePasswordParams(parameters) if (globals.IastK8sMode) { K8sPropagatorUtils.addModificationInfo(source, target, parameters) } return target[IastProperties.property.TAINTED_DATA] } module.exports.registerTaint = (obj, taintedObjectData) => { IastProperties.definePropertyWithValue(obj, IastProperties.property.TAINTED_DATA, taintedObjectData) } module.exports.propagateTaint = (source, target, obj, methodSignature, argumentList, returnValue) => { const params = StackInfo.getParamsStringArray(obj, obj, methodSignature, argumentList, returnValue) const res = module.exports.propagateTaintWithParameters(source, target, params) return res } module.exports.propagateTaintWithParameters = (source, target, parameters) => { return doPropagateTaint(source, target, parameters) } module.exports.propagateArrayEntriesTaint = (sourceArray, target, parameters) => { let result = null for (const arg of sourceArray) { let newTaintData = doPropagateTaint(arg, target, parameters) result = newTaintData != null ? newTaintData : result } return result } module.exports.propagateTaintToArrayEntries = (source, targetArray, parameters) => { for (const target of targetArray) { doPropagateTaint(source, target, parameters) } } // recursively traverse the object and search for tainted objects module.exports.propagateTaintFromObject = (obj, target, parameters) => { return propagateTaintFromObjectRecursive(obj, target, parameters) } function propagateTaintFromObjectRecursive(obj, target, parameters, memoization){ // found a tainted object (e.g. String): if (target !== Object(target)) return null memoization = memoization || new Set() if (memoization.has(obj)) return null memoization.add(obj) let res = null if (module.exports.isItemTainted(obj)) { res = doPropagateTaint(obj, target, parameters) } else if (typeof obj === 'string') { // no taint on string primitives + it is iterated by letter if it reaches the object code } else { for (const property in obj) { if (Object.prototype.hasOwnProperty.call(obj, property) && obj[property] != null) { const newTaintData = propagateTaintFromObjectRecursive(obj[property], target, parameters, memoization) res = newTaintData != null ? newTaintData : res } } } return res } // recursively checks if there is any tainted item in an object module.exports.isObjectTainted = (obj, memoization) => { return recursivelyCheckPropertiesOnObject(obj, memoization, isItemTainted) } // recursively checks if there is any tainted or sanitized item in an object module.exports.ObjectHasTaintedData = (obj, memoization) => { return recursivelyCheckPropertiesOnObject(obj, memoization, hasTaintedData) } function recursivelyCheckPropertiesOnObject(obj, memoization, conditionFunction) { memoization = memoization || new Set() if (memoization.has(obj)) return false memoization.add(obj) if (obj == null || typeof obj === 'string' || typeof obj === 'function') { return false } // must return on string otherwise will go char by char if (conditionFunction(obj)) { return true } if (obj instanceof String) { return false } // prevent traversing char by char for String Object. String Object doesn't contain taint as its values are chars. // recursively traverse fields else { for (const property in obj) { if (recursivelyCheckPropertiesOnObject(obj[property], memoization, conditionFunction)) { return true } } } return false } const isItemTainted = (obj) => { if (obj == null || obj[IastProperties.property.TAINTED_DATA] == null) { return false } return obj[IastProperties.property.TAINTED_DATA].isTainted() } const hasTaintedData = (obj) => { return obj != null && obj[IastProperties.property.TAINTED_DATA] !== undefined } // keep track of body to know the index of tainted data // currently not needed function updateIastIndex (data, req) { // maybe we can get the length directly from body property if (req[IastProperties.property.INFO] === undefined) { IastProperties.definePropertyWithValue(req, IastProperties.property.INFO, 0) } req[IastProperties.property.INFO] += data.length } function addXssSinkTask (xssSinkTask, req) { if (req != null) { if (!IastProperties.property.SINK_TASKS in req) { IastProperties.definePropertyWithValue(req, IastProperties.property.SINK_TASKS, []) } req[IastProperties.property.SINK_TASKS].push(xssSinkTask) } } const sinkTrigger = (obj, vulnerability, parameters, stack, memoization) => { memoization = memoization || new Set() if (memoization.has(obj)) return false memoization.add(obj) if (obj == null || typeof obj === 'string') return false if (obj[IastProperties.property.TAINTED_DATA] !== undefined) { return doSinkTrigger(obj, vulnerability, parameters) } // recursively traverse fields let retValue = false // object for (const property in obj) { retValue = sinkTrigger(obj[property], vulnerability, parameters, stack, memoization) || retValue } return retValue } function doSinkTrigger(obj, vulnerability, parameters) { let retValue = false const data = obj[IastProperties.property.TAINTED_DATA] let processedSinkStack = null let dastReportedHashes = new Set() if (data != null) { const sinkStackInfo = new StackInfo(HookRuleType.SINK, parameters, vulnerability, new global.origError()) for (const flow of data.flows) { // add sink report to dast response header and potentially update processedSinkStack processedSinkStack = IastDastSinkReporter.addSinkReport(flow, vulnerability, processedSinkStack, dastReportedHashes) if (processedSinkStack != null) { // save the edited stack, so we don't need to create a new one again when the issue is reported flow.setSinkStackString(processedSinkStack); sinkStackInfo.setStackStr(processedSinkStack); } if (globals.IastK8sMode) { telemetrySinkTrigger(obj, sinkStackInfo, flow, vulnerability, parameters) K8sSinkUtils.addSinkAdditionalInfo(obj, parameters.methodSignature, parameters.className) } if (!flow.isTaintedForVulnerability(vulnerability)) { continue } if (vulnerability === (Vulnerability.PATH_TRAVERSAL)) { logger.debug(`Creating PathTraversalSinkTask for vulnerability ${vulnerability}`) const task = new PathTraversalSinkTask(flow, vulnerability, new global.origError(), parameters, obj) retValue = TasksManager.pushToQueue(task) || retValue } else if (vulnerability === (Vulnerability.SQL_INJECTION)) { logger.debug(`Creating SqlSinkTask for vulnerability ${vulnerability}`) // Run Sink test in separate thread const task = new SqlSinkTask(flow, vulnerability, new global.origError(), parameters, obj) retValue = TasksManager.pushToQueue(task) || retValue } else if (vulnerability === Vulnerability.COMMAND_INJECTION_BASH) { logger.debug(`Creating BashOsCommandingSinkTask for vulnerability ${vulnerability}`) const task = new BashOsCommandingSinkTask(flow, vulnerability, new global.origError(), parameters, obj) retValue = TasksManager.pushToQueue(task) || retValue } else if (vulnerability === Vulnerability.COMMAND_INJECTION_CMD){ logger.debug(`Creating CmdOsCommandingSinkTask for vulnerability ${vulnerability}`) const task = new CmdOsCommandingSinkTask(flow, vulnerability, new global.origError(), parameters, obj) retValue = TasksManager.pushToQueue(task) || retValue } else if (vulnerability === Vulnerability.COMMAND_INJECTION_POWERSHELL){ logger.debug(`Creating PowerShellOsCommandingSinkTask for vulnerability ${vulnerability}`) const task = new PowerShellOsCommandingSinkTask(flow, vulnerability, new global.origError(), parameters, obj) retValue = TasksManager.pushToQueue(task) || retValue } else if (vulnerability === (Vulnerability.XSS)) { logger.debug(`Creating XssSinkTask for vulnerability ${vulnerability}`) // Run Sink test in separate thread const task = new XssSinkTask(flow, vulnerability, new global.origError(), parameters) // addXssSinkTask(task, sinkInfo) // updateIastIndex(obj, sinkInfo) retValue = TasksManager.pushToQueue(task) || retValue } else if (vulnerability === (Vulnerability.PASSWORD_LEAKAGE_SENT_DATA) || vulnerability === (Vulnerability.PASSWORD_LEAKAGE_DB)){ if (flow.entity.isPassword) retValue = sinkTriggerWithoutTask(sinkStackInfo, flow, vulnerability) || retValue } else if (vulnerability === (Vulnerability.MISSING_URL_VALIDATION)){ retValue = sinkTriggerWithoutTask(sinkStackInfo, flow, vulnerability) || retValue } // all unimplemented vulnerabilities - report only if task list is empty else if (flow.taskList.length === 0) { retValue = sinkTriggerWithoutTask(sinkStackInfo, flow, vulnerability) || retValue } else { logger.debug(`Creating UnimplementedSinkTask for vulnerability ${vulnerability}`) // Run Sink test in separate thread const task = new UnimplementedSinkTask(flow, vulnerability, new global.origError(), parameters) retValue = TasksManager.pushToQueue(task) || retValue } } } return retValue } function telemetrySinkTrigger(sinkObj, sinkStackInfo, flow, origVulnerability) { const origStackInfo = flow.stackInfoList if (origVulnerability === Vulnerability.PASSWORD_LEAKAGE_SENT_DATA) { const telemetrySource = Utils.copyObject(flow.stackInfoList[0]) const telemetrySink = Utils.copyObject(sinkStackInfo) telemetrySource.rawStack = null telemetrySink.rawStack = null flow.stackInfoList = [telemetrySource, telemetrySink] } else { flow.stackInfoList = [...flow.stackInfoList, sinkStackInfo] } flow.addAdditionalInfo({[keys.VULNERABILITY]: origVulnerability}) flow.updateStackAndHashForReporting(Vulnerability.TELEMETRY) reportVulnerabilityWithFlow(flow, Vulnerability.TELEMETRY) delete flow.additionalInfo[keys.VULNERABILITY] flow.stackInfoList = origStackInfo } function performPathTraversalSink(flow, vulnerability, stack, parameters, sinkStr){ let is_vulnerable = false let origVal = flow.entity.value let sink = sinkStr if (sink.endsWith(origVal)) { is_vulnerable = true } else { let sinkWithoutExtension = sink.origSplit('.').origSlice(0, -1).origJoin('.') if (sinkWithoutExtension !== '' && sinkWithoutExtension.endsWith(origVal)) { is_vulnerable = true } } if (is_vulnerable) { module.exports.sinkTaskTrigger(flow, vulnerability, sink, true, stack, parameters) } } module.exports.sinkTaskTrigger = (flow, vulnerability, sinkInfoGenerator, hadSinkTask, sinkRawStack, parameters, additionalInfo = null ) => { if (!flow.isTaintedForVulnerability(vulnerability)) { return false } if (!flow.isReported(vulnerability)) { const stackInfo = new StackInfo(HookRuleType.SINK, parameters, vulnerability, sinkRawStack); const sinkStackString = flow.getSinkStackString(); if(sinkStackString != null){ stackInfo.setStackStr(sinkStackString); } flow.addToStackInfoList(stackInfo); flow.updateStackAndHashForReporting(vulnerability) if (sinkInfoGenerator != null){ const sinkInfoValue = sinkInfoGenerator.generateAdditionalInfo() if (sinkInfoValue != null){ const additionalSinkInfo = {[AdditionalInfoObj.keys.SINK_INFO]: sinkInfoValue} additionalInfo = Object.assign({}, additionalInfo, additionalSinkInfo) additionalInfo[AdditionalInfoObj.keys.VULNERABLE_CHARS] = sinkInfoGenerator.getVulnerableCharsAsString() additionalInfo[AdditionalInfoObj.keys.EXPLOIT_EXAMPLE] = sinkInfoGenerator.getExploitExample(flow.entity) } } reportVulnerabilityWithFlow(flow, vulnerability, additionalInfo) flow.stackInfoList.pop() } return true } const sinkTriggerWithoutTask = (rawStackInfo, flow, vulnerability) => { flow.addToStackInfoList(rawStackInfo) if (!flow.isTaintedForVulnerability(vulnerability)) { return false } if (!flow.isReported(vulnerability)) { flow.updateStackAndHashForReporting(vulnerability) reportVulnerabilityWithFlow(flow, vulnerability) } flow.stackInfoList.pop() return true } module.exports.reportExploitVulnerability = (v, object, signature, args, returnValue, additionalInfo = null) => { reportVulnerability(v, object, signature, args, returnValue, true, false, null, null, additionalInfo) } module.exports.reportExploitVulnerabilityWithEntity = (v, signature, entity, additionalInfo = null, stackTrace = null) => { reportVulnerability(v, null, signature, null, null, true, false, entity, null, additionalInfo, stackTrace) } const reportStackLessVulnerability = (v, object, signature, args, returnValue, reportPerRequest, entity, requestInfo, additionalInfo = null) => { if (signature == null || signature === ''){ signature = requestInfo.fullUrlWithoutQuery } reportVulnerability(v, object, signature, args, returnValue, false, reportPerRequest, entity, requestInfo, additionalInfo) } function reportVulnerability (v, object, signature, args, returnValue, hasStack, reportPerRequest, entity = null, requestInfo = null, additionalInfo = null, stackTrace = null) { entity = entity != null ? entity : new Entity.Entity('', '', Entity.EntityType.NO_TYPE) const ruleType = v === Vulnerability.OPEN_SOURCE_IAST ? HookRuleType.OPEN_SOURCE_LIBRARY : v === Vulnerability.DETECTED_APIS ? HookRuleType.API_ENDPOINTS : HookRuleType.EXPLOIT const stackInfo = new StackInfo(ruleType, StackInfo.getParamsStringArrayPostHook(object, object, signature, args, returnValue), null, stackTrace == null && hasStack ? new global.origError() : stackTrace) stackInfo.updateStack() const strStack = hasStack ? stackInfo.getStackTraceString() : '' if (hasStack && strStack === '') { return // this can happen if an exploit happened in a library called by the agent } additionalInfo = addKubernetesAdditionalInfo(additionalInfo) if (!ConfigInfo.isIgnoredVulnerability(v) && !ConfigInfo.isIgnoredMethod(strStack, v) && !ignoreCookieForVulnerability(entity, v)) { let hashObj = Utils.createHashObject() hashObj.update(v) hashObj.update(stackInfo.updateHash(v)) if (entity.name !== ''){ hashObj.update(entity.name) } if (additionalInfo != null){ hashObj.update(JSON.origStringify(additionalInfo)) } if (reportPerRequest) { hashObj.update(requestInfo.uri) } const info = new VulnerabilityInfo(requestInfo, entity, v, hashObj.produce(), [stackInfo], new Date(), additionalInfo) Distributor.pushVulnerabilityInfo(info) } } function reportVulnerabilityWithFlow (flow, v, additionalInfo = null) { flow.addToReported(v) if (!ConfigInfo.isIgnoredVulnerability(v) && !ConfigInfo.isIgnoredMethod(flow.getStackString(), v) && !ignoreCookieForVulnerability(flow.FlowEntity, v)) { additionalInfo = addKubernetesAdditionalInfo(additionalInfo) //handles case of additionalInfo is null additionalInfo = Object.assign({}, additionalInfo, flow.additionalInfo) const info = new VulnerabilityInfo(flow.requestInfo, flow.entity, v, flow.getHashValue(), flow.getStackArray(), new Date(), additionalInfo) Distributor.pushVulnerabilityInfo(info) } } module.exports.sanitizeAll = (obj) => { if (hasTaintedData(obj)) { const taintedData = obj[IastProperties.property.TAINTED_DATA] taintedData.addAdditionalInfoToFlows({[keys.SANITIZED]: "Sanitized Flow"}) } } module.exports.ReportOnServerHeadersVulnerability = (headers, requestInfo) => { for (const header of headers) { const lowercaseHeader = header.origToLowerCase() const headerInfoObj = { [AdditionalInfoObj.keys.UNNECESSARY_RESPONSE_HEADER]: header } if (lowercaseHeader.origStringIncludes('server')) { reportStackLessVulnerability(Vulnerability.SERVER_HEADER, null, null, [], null, false, null, requestInfo, headerInfoObj) } if (lowercaseHeader.origStringIncludes('x-powered-by')) { reportStackLessVulnerability(Vulnerability.XPOWEREDBY_HEADER, null, null, [], null, false, null, requestInfo, headerInfoObj) } } } module.exports.updateEntity = (taintedObj, entityName, entityValue, entityType) => { if (taintedObj != null && hasTaintedData(taintedObj)) { taintedObj[IastProperties.property.TAINTED_DATA].updateEntity(entityName, entityValue, entityType) } } function addKubernetesAdditionalInfo(additionalInfo) { const podName = process.env["K8S_POD_NAME"] if (podName != null) { additionalInfo = Object.assign({}, additionalInfo, {[AdditionalInfoObj.keys.K8S_POD_NAME]: podName}) } return additionalInfo } module.exports.reportStackLessVulnerability = reportStackLessVulnerability module.exports.reportVulnerability = reportVulnerability module.exports.HookRuleType = HookRuleType module.exports.Vulnerability = Vulnerability module.exports.getTaintedData = getTaintedData module.exports.isItemTainted = isItemTainted module.exports.hasTaintedData = hasTaintedData module.exports.sinkTrigger = sinkTrigger