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