@hclsoftware/secagent
Version:
IAST agent
183 lines (162 loc) • 9.14 kB
JavaScript
//IASTIGNORE
/*
* ****************************************************
* Licensed Materials - Property of HCL.
* (c) Copyright HCL Technologies Ltd. 2017, 2025.
* Note to U.S. Government Users *Restricted Rights.
* ****************************************************
*/
const IastProperties = require("../../Hooks/IastProperties")
const TaintTracker = require("../../TaintTracker")
const {ConfigInfo} = require("../../ConfigFile/ConfigInfo")
const StackInfo = require("../../StackInfo")
const SessionTracker = require("../../SessionTracker")
const Utils = require("../../Utils/Utils")
const taintedObjectData = require("../../TaintedObjectData")
const {keys} = require("../../AdditionalInfo")
const globals = require("../../Globals")
const TagsUtils = require("../../TagsUtils")
const Entity = require('../../Entity')
const IastLogger = require("../../Logger/IastLogger");
let currentRequestInfo = null
function getRequestInfo() {
return currentRequestInfo
}
function setRequestInfo(requestInfo) {
currentRequestInfo = requestInfo
}
function getProxyForSourceObject (requestInfo, sourceObj, objName, propertyName, entityType, prefix = '', stack = new global.origError()) {
if (sourceObj == null || (typeof sourceObj == 'object' && (sourceObj.length === 0) || sourceObj[IastProperties.property.SOURCE_PROXY])) {
return sourceObj
}
// might be a buffer/string, e.g. when using body parser middleware for binary/text content type - request.body is of type buffer/string.
if (typeof sourceObj === 'string' || Buffer.isBuffer(sourceObj)) {
return getTaintedCopyOfProperty(requestInfo, `${objName}.${propertyName}`, '[[Get]]', propertyName, propertyName, sourceObj, entityType, stack)
}
/* parseInt (and maybe other functions) on tainted array throws the following error: 'Cannot convert object to
* primitive value'. This is because parseInt try to convert array to string by calling to toPrimitive and when there is
* no implementation for it, parseInt will call to join, which returns a tainted string (of type object), because join
* is a propagator, and so it cannot convert the array to string.
* We solve that by adding toPrimitive handler for array sources.
* Examples:
* parseInt(['3', '5']) // 3
* parseInt(['123bla', '5']) // 123
* parseInt(['bla3', '5']) // NaN
* parseInt([]) // NaN
*/
else if (Array.isArray(sourceObj)) {
sourceObj[Symbol.toPrimitive] = (hint) => {
return sourceObj.origJoin()
}
}
// The `get` method in this Proxy that is set on 'sourceObj', intercepts property access on `sourceObj`.
// And returns either the original, a modified tainted version of the actual property.
return new Proxy(sourceObj, {
get(target, sourceName, receiver) {
if (sourceName === IastProperties.property.SOURCE_PROXY) return true
const useTargetAsReceiver = target instanceof Uint8Array // should use original this for Uint8Array (e.g. buffers)
let sourceValue = Reflect.get(target, sourceName, useTargetAsReceiver ? target : receiver)
if (!['object', 'string'].origArrayIncludes(typeof sourceValue)){
return sourceValue
}
// if new tainted copy needs to be created for any special case, we try to remove tainted data object in the sourceValue
// so that it is clean for creation of new tainted copy
if (TaintTracker.isItemTainted(sourceValue)) {
if (!shouldReplaceToCookieEntity(sourceValue, entityType, sourceObj)) return sourceValue
// cleaning sourceValue off tainted data Object
if (sourceValue instanceof String) sourceValue = sourceValue.origToString()
else if (typeof sourceValue === 'object') {
// assuming only tainted data is present and removing it
try {
delete sourceValue[IastProperties.property.TAINTED_DATA]
} catch (e) {
IastLogger.eventLog.error(`error removing TAINTED_DATA property, Error: ${e}`)
}
}
} else if (TaintTracker.hasTaintedData(sourceValue)){
// Additional logging will be needed here to gain more insight into the sourceValue when this occurs.
// sourceValue is sanitized!. so we need not create new tainted copy)
return sourceValue
}
const isArray = Array.isArray(target);
const entityName = prefix === '' ? sourceName : (isArray ? `${prefix}[${sourceName}]` : `${prefix}.${sourceName}`)
if (entityType === Entity.EntityType.BODY && entityName !== "body"){
// means, it's an inner property inside the body. So, we set it as BodyParameter type
entityType = Entity.EntityType.BODY_PARAMETER;
}
// a check for the header/cookie listed in the config file for filtering. returns whatever the type original method returns
if(ConfigInfo.isSafeHeaderOrSafeCookie(entityName, entityType)){
return sourceValue
}
if (typeof sourceValue === 'string') {
const strOfObj = prefix === '' ? `${objName}.${propertyName}` : `${objName}.${propertyName}.${prefix}`
storeParameterOrHeaderOrCookieName(requestInfo, sourceName, entityType)
return getTaintedCopyOfProperty(requestInfo, strOfObj, '[[Get]]', sourceName, entityName, sourceValue, entityType, stack)
} else {
return getProxyForSourceObject(requestInfo, sourceValue, objName, propertyName, entityType, entityName, stack)
}
}
})
}
function shouldReplaceToCookieEntity (sourceValue, entityType, sourceObj) {
// Goal: Create a tainted copy specifically with a cookie entity type, rather than returning the original sourceValue.
// Process:
// - If the entity type from tainted data is 'header' and the current entity in the proxy object is 'cookie':
// - First, check if the proxy object's entity type is 'cookie'.
// - If it’s not 'cookie', return false.
// - If it is 'cookie', proceed to validate flows:
// - Check if there is a single flow; if there are multiple flows, log an error and return false.
// - If the flow's entity type is 'header', return true. In all other cases, return false.
// - Log a warning for any other cases and return false.
if (entityType !== Entity.EntityType.COOKIE) return false
const taintedData = TaintTracker.getTaintedData(sourceValue);
const flows = taintedData.flows
// given that sourceValue is tainted we check for a case where flows are more than one
if (flows && flows.length > 1) {
IastLogger.eventLog.error('Cookie source with multiple existing flows detected. Ignoring the cookie entity.');
return false;
}
return flows && flows[0].entity.type === Entity.EntityType.HEADER;
}
function storeParameterOrHeaderOrCookieName (requestInfo, name, entityType) {
if (requestInfo == null) {
return
}
if (entityType === Entity.EntityType.PARAMETER || entityType === Entity.EntityType.BODY_PARAMETER) {
requestInfo.addUsedParameter(name)
} else if (entityType === Entity.EntityType.HEADER) {
requestInfo.addUsedHeader(name)
} else if (entityType === Entity.EntityType.COOKIE){
requestInfo.addUsedCookie(name)
}
}
function getTaintedCopyOfProperty (requestInfo, thatStr, methodDescriptor, args, entityName, objToTaint, entityType, stack) {
let taintedObj;
let entityValue;
if (Buffer.isBuffer(objToTaint)) {
entityValue = objToTaint.toString()
// the object is a buffer, we don't crete a new String object, but rather use the original buffer
taintedObj = objToTaint
} else {
entityValue = objToTaint;
taintedObj = new String(objToTaint)
}
const params = StackInfo.getSimpleParamsStringArray(null, thatStr, methodDescriptor, Array.isArray(args) ? args : [args], entityValue)
if (ConfigInfo.ConfigInfo.hidePasswords && SessionTracker.isPasswordName(entityName)) {
params.ret = Utils.PASSWORD_TEXT
}
const stackInfo = new StackInfo(TaintTracker.HookRuleType.SOURCE, params, null, stack)
const taintedData = taintedObjectData.taintedObjectDataWithFlow(requestInfo, entityName, entityValue, entityType)
if (requestInfo.serverFlowTags != null) {
taintedData.addAdditionalInfoToFlows({[keys.IAST_TAG]: requestInfo.serverFlowTags})
} else if (globals.IastK8sMode) {
TagsUtils.addNewTagToFlows(taintedData.flows)
}
taintedData.addToStackList(stackInfo)
TaintTracker.registerTaint(taintedObj, taintedData)
return taintedObj
}
module.exports.getProxyForSourceObject = getProxyForSourceObject
module.exports.getTaintedCopyOfProperty = getTaintedCopyOfProperty
module.exports.getRequestInfo = getRequestInfo
module.exports.setRequestInfo = setRequestInfo