dd-trace
Version:
Datadog APM tracing client for JavaScript
259 lines (223 loc) • 7.46 kB
JavaScript
const dc = require('dc-polyfill')
const TaintedUtils = require('@datadog/native-iast-taint-tracking')
const { storage } = require('../../../../../datadog-core')
const iastContextFunctions = require('../iast-context')
const { EXECUTED_PROPAGATION } = require('../telemetry/iast-metric')
const { isDebugAllowed } = require('../telemetry/verbosity')
const log = require('../../../log')
const { taintObject } = require('./operations-taint-object')
const mathRandomCallCh = dc.channel('datadog:random:call')
const evalCallCh = dc.channel('datadog:eval:call')
const JSON_VALUE = 'json.value'
function noop (res) { return res }
// NOTE: methods of this object must be synchronized with csi-methods.js file definitions!
// Otherwise you may end up rewriting a method and not providing its rewritten implementation
const TaintTrackingNoop = {
concat: noop,
eval: noop,
join: noop,
parse: noop,
plusOperator: noop,
random: noop,
replace: noop,
slice: noop,
substr: noop,
substring: noop,
stringCase: noop,
tplOperator: noop,
trim: noop,
trimEnd: noop,
}
function getTransactionId (iastContext) {
return iastContext?.[iastContextFunctions.IAST_TRANSACTION_ID]
}
function getContextDefault () {
const store = storage('legacy').getStore()
return iastContextFunctions.getIastContext(store)
}
function getContextDebug () {
const iastContext = getContextDefault()
EXECUTED_PROPAGATION.inc(iastContext)
return iastContext
}
function getFilteredCsiFn (cb, filter, getContext) {
return function csiCall (res, fn, target, ...rest) {
try {
if (filter(res, fn, target)) { return res }
const context = getContext()
const transactionId = getTransactionId(context)
if (transactionId) {
return cb(transactionId, res, target, ...rest)
}
} catch (e) {
log.error('[ASM] Error invoking CSI %s', target, e)
}
return res
}
}
function notString () {
return Array.prototype.some.call(arguments, (p) => typeof p !== 'string')
}
function isValidCsiMethod (fn, protos) {
return protos.includes(fn)
}
function getCsiFn (cb, getContext, ...protos) {
let filter
if (!protos || protos.length === 0) {
filter = (res, fn, target) => notString(res, target)
} else if (protos.length === 1) {
const protoFn = protos[0]
filter = (res, fn, target) => notString(res, target) || fn !== protoFn
} else {
filter = (res, fn, target) => notString(res, target) || !isValidCsiMethod(fn, protos)
}
return getFilteredCsiFn(cb, filter, getContext)
}
function csiMethodsDefaults (names, excluded, getContext) {
const impl = {}
for (const name of names) {
if (excluded.includes(name)) continue
impl[name] = getCsiFn(
(transactionId, res, target, ...rest) => TaintedUtils[name](transactionId, res, target, ...rest),
getContext,
String.prototype[name]
)
}
return impl
}
function csiMethodsOverrides (getContext) {
return {
plusOperator: function (res, op1, op2) {
try {
if (notString(res) || (notString(op1) && notString(op2))) { return res }
const iastContext = getContext()
const transactionId = getTransactionId(iastContext)
if (transactionId) {
return TaintedUtils.concat(transactionId, res, op1, op2)
}
} catch (e) {
log.error('[ASM] Error invoking CSI plusOperator', e)
}
return res
},
tplOperator: function (res, ...rest) {
try {
const iastContext = getContext()
const transactionId = getTransactionId(iastContext)
if (transactionId) {
return TaintedUtils.concat(transactionId, res, ...rest)
}
} catch (e) {
log.error('[ASM] Error invoking CSI tplOperator', e)
}
return res
},
stringCase: getCsiFn(
(transactionId, res, target) => TaintedUtils.stringCase(transactionId, res, target),
getContext,
String.prototype.toLowerCase,
String.prototype.toUpperCase
),
trim: getCsiFn(
(transactionId, res, target) => TaintedUtils.trim(transactionId, res, target),
getContext,
String.prototype.trim,
String.prototype.trimStart
),
random: function (res, fn) {
if (mathRandomCallCh.hasSubscribers) {
mathRandomCallCh.publish({ fn })
}
return res
},
eval: function (res, fn, target, script) {
// eslint-disable-next-line no-eval
if (evalCallCh.hasSubscribers && fn === globalThis.eval) {
evalCallCh.publish({ script })
}
return res
},
parse: function (res, fn, target, json) {
if (fn === JSON.parse) {
try {
const iastContext = getContext()
const transactionId = getTransactionId(iastContext)
if (transactionId) {
const ranges = TaintedUtils.getRanges(transactionId, json)
// TODO: first version.
// here we are losing the original source because taintObject always creates a new tainted
if (ranges?.length > 0) {
const range = ranges.find(range => range.iinfo?.type)
res = taintObject(iastContext, res, range?.iinfo.type || JSON_VALUE)
}
}
} catch (e) {
log.error('[ASM] Error invoking CSI JSON.parse', e)
}
}
return res
},
join: function (res, fn, target, separator) {
if (fn === Array.prototype.join) {
try {
const iastContext = getContext()
const transactionId = getTransactionId(iastContext)
if (transactionId) {
res = TaintedUtils.arrayJoin(transactionId, res, target, separator)
}
} catch (e) {
log.error('[ASM] Error invoking CSI join', e)
}
}
return res
},
}
}
function createImplWith (getContext) {
const methodNames = Object.keys(TaintTrackingNoop)
const overrides = csiMethodsOverrides(getContext)
// impls could be cached but at the moment there is only one invocation to getTaintTrackingImpl
return {
...csiMethodsDefaults(methodNames, Object.keys(overrides), getContext),
...overrides,
}
}
function getTaintTrackingImpl (telemetryVerbosity, dummy = false) {
if (dummy) return TaintTrackingNoop
// with Verbosity.DEBUG every invocation of a TaintedUtils method increases the EXECUTED_PROPAGATION metric
return isDebugAllowed(telemetryVerbosity)
? createImplWith(getContextDebug)
: createImplWith(getContextDefault)
}
function getTaintTrackingNoop () {
return getTaintTrackingImpl(null, true)
}
const lodashFns = {
join: TaintedUtils.arrayJoin,
toLower: TaintedUtils.stringCase,
toUpper: TaintedUtils.stringCase,
trim: TaintedUtils.trim,
trimEnd: TaintedUtils.trimEnd,
trimStart: TaintedUtils.trim,
}
function getLodashTaintedUtilFn (lodashFn) {
return lodashFns[lodashFn] || ((transactionId, result) => result)
}
function lodashTaintTrackingHandler (message) {
try {
if (!message.result) return
const context = getContextDefault()
const transactionId = getTransactionId(context)
if (transactionId) {
message.result = getLodashTaintedUtilFn(message.operation)(transactionId, message.result, ...message.arguments)
}
} catch (e) {
log.error('[ASM] Error invoking CSI lodash %s', message.operation, e)
}
}
module.exports = {
getTaintTrackingImpl,
getTaintTrackingNoop,
lodashTaintTrackingHandler,
}