UNPKG

@hclsoftware/secagent

Version:

IAST agent

183 lines (162 loc) 9.14 kB
//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