dd-trace
Version:
Datadog APM tracing client for JavaScript
297 lines (251 loc) • 11.1 kB
JavaScript
'use strict'
/* eslint-disable unicorn/prefer-string-slice */
const log = require('../../../../log')
const vulnerabilities = require('../../vulnerabilities')
const { defaults } = require('../../../../config/defaults')
const { contains, intersects, remove } = require('./range-utils')
const commandSensitiveAnalyzer = require('./sensitive-analyzers/command-sensitive-analyzer')
const hardcodedPasswordAnalyzer = require('./sensitive-analyzers/hardcoded-password-analyzer')
const jsonSensitiveAnalyzer = require('./sensitive-analyzers/json-sensitive-analyzer')
const ldapSensitiveAnalyzer = require('./sensitive-analyzers/ldap-sensitive-analyzer')
const sqlSensitiveAnalyzer = require('./sensitive-analyzers/sql-sensitive-analyzer')
const taintedRangeBasedSensitiveAnalyzer = require('./sensitive-analyzers/tainted-range-based-sensitive-analyzer')
const urlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyzer')
const REDACTED_SOURCE_BUFFER = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
class SensitiveHandler {
constructor () {
this._namePattern = new RegExp(/** @type {string} */ (defaults['iast.redactionNamePattern']), 'gmi')
this._valuePattern = new RegExp(/** @type {string} */ (defaults['iast.redactionValuePattern']), 'gmi')
this._sensitiveAnalyzers = new Map()
this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, taintedRangeBasedSensitiveAnalyzer)
this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, commandSensitiveAnalyzer)
this._sensitiveAnalyzers.set(vulnerabilities.HARDCODED_PASSWORD, (evidence) => {
return hardcodedPasswordAnalyzer(evidence, this._valuePattern)
})
this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, ldapSensitiveAnalyzer)
this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer)
this._sensitiveAnalyzers.set(vulnerabilities.SQL_INJECTION, sqlSensitiveAnalyzer)
this._sensitiveAnalyzers.set(vulnerabilities.SSRF, urlSensitiveAnalyzer)
this._sensitiveAnalyzers.set(vulnerabilities.TEMPLATE_INJECTION, taintedRangeBasedSensitiveAnalyzer)
this._sensitiveAnalyzers.set(vulnerabilities.UNTRUSTED_DESERIALIZATION, taintedRangeBasedSensitiveAnalyzer)
this._sensitiveAnalyzers.set(vulnerabilities.UNVALIDATED_REDIRECT, urlSensitiveAnalyzer)
}
isSensibleName (name) {
this._namePattern.lastIndex = 0
return this._namePattern.test(name)
}
isSensibleValue (value) {
this._valuePattern.lastIndex = 0
return this._valuePattern.test(value)
}
isSensibleSource (source) {
return source != null && (this.isSensibleName(source.name) || this.isSensibleValue(source.value))
}
scrubEvidence (vulnerabilityType, evidence, sourcesIndexes, sources) {
const sensitiveAnalyzer = this._sensitiveAnalyzers.get(vulnerabilityType)
if (sensitiveAnalyzer) {
const sensitiveRanges = sensitiveAnalyzer(evidence)
if (evidence.ranges || sensitiveRanges?.length) {
return this.toRedactedJson(evidence, sensitiveRanges, sourcesIndexes, sources)
}
}
return null
}
toRedactedJson (evidence, sensitive, sourcesIndexes, sources) {
const valueParts = []
const redactedSources = []
const redactedSourcesContext = []
const { value, ranges } = evidence
let start = 0
let nextTaintedIndex = 0
let sourceIndex
let nextTainted = ranges?.shift()
let nextSensitive = sensitive.shift()
for (let i = 0; i < value.length; i++) {
if (nextTainted != null && nextTainted.start === i) {
this.writeValuePart(valueParts, value.slice(start, i), sourceIndex)
sourceIndex = sourcesIndexes[nextTaintedIndex]
while (nextSensitive != null && contains(nextTainted, nextSensitive)) {
const redactionStart = nextSensitive.start - nextTainted.start
const redactionEnd = nextSensitive.end - nextTainted.start
if (redactionStart === redactionEnd) {
this.writeRedactedValuePart(valueParts, 0)
} else {
this.redactSource(
sources,
redactedSources,
redactedSourcesContext,
sourceIndex,
redactionStart,
redactionEnd
)
}
nextSensitive = sensitive.shift()
}
if (nextSensitive != null && intersects(nextSensitive, nextTainted)) {
const redactionStart = nextSensitive.start - nextTainted.start
const redactionEnd = nextSensitive.end - nextTainted.start
this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd)
const entries = remove(nextSensitive, nextTainted)
nextSensitive = entries.length > 0 ? entries[0] : null
}
if (this.isSensibleSource(sources[sourceIndex]) && !sources[sourceIndex].redacted) {
redactedSources.push(sourceIndex)
sources[sourceIndex].pattern = ''.padEnd(sources[sourceIndex].value.length, REDACTED_SOURCE_BUFFER)
sources[sourceIndex].redacted = true
}
if (redactedSources.includes(sourceIndex)) {
const partValue = value.slice(i, i + (nextTainted.end - nextTainted.start))
this.writeRedactedValuePart(
valueParts,
partValue.length,
sourceIndex,
partValue,
sources[sourceIndex],
redactedSourcesContext[sourceIndex],
this.isSensibleSource(sources[sourceIndex])
)
redactedSourcesContext[sourceIndex] = []
} else {
const substringEnd = Math.min(nextTainted.end, value.length)
this.writeValuePart(valueParts, value.slice(nextTainted.start, substringEnd), sourceIndex)
}
start = i + (nextTainted.end - nextTainted.start)
// eslint-disable-next-line sonarjs/updated-loop-counter -- skip ahead; outer `i++` advances to `start`
i = start - 1
nextTainted = ranges.shift()
nextTaintedIndex++
sourceIndex = null
} else if (nextSensitive != null && nextSensitive.start === i) {
this.writeValuePart(valueParts, value.slice(start, i), sourceIndex)
if (nextTainted != null && intersects(nextSensitive, nextTainted)) {
sourceIndex = sourcesIndexes[nextTaintedIndex]
const redactionStart = nextSensitive.start - nextTainted.start
const redactionEnd = nextSensitive.end - nextTainted.start
this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd)
for (const entry of remove(nextSensitive, nextTainted)) {
if (entry.start === i) {
nextSensitive = entry
} else {
sensitive.unshift(entry)
}
}
}
const _length = nextSensitive.end - nextSensitive.start
this.writeRedactedValuePart(valueParts, _length)
start = i + _length
// eslint-disable-next-line sonarjs/updated-loop-counter -- skip ahead; outer `i++` advances to `start`
i = start - 1
nextSensitive = sensitive.shift()
}
}
if (start < value.length) {
this.writeValuePart(valueParts, value.slice(start))
}
return { redactedValueParts: valueParts, redactedSources }
}
redactSource (sources, redactedSources, redactedSourcesContext, sourceIndex, start, end) {
if (sourceIndex != null) {
if (!sources[sourceIndex].redacted) {
redactedSources.push(sourceIndex)
sources[sourceIndex].pattern = ''.padEnd(sources[sourceIndex].value.length, REDACTED_SOURCE_BUFFER)
sources[sourceIndex].redacted = true
}
if (!redactedSourcesContext[sourceIndex]) {
redactedSourcesContext[sourceIndex] = []
}
redactedSourcesContext[sourceIndex].push({
start,
end,
})
}
}
writeValuePart (valueParts, value, source) {
if (value.length > 0) {
if (source == null) {
valueParts.push({ value })
} else {
valueParts.push({ value, source })
}
}
}
writeRedactedValuePart (
valueParts,
length,
sourceIndex,
partValue,
source,
sourceRedactionContext,
isSensibleSource
) {
if (sourceIndex == null) {
valueParts.push({ redacted: true })
} else {
const placeholder = source.value.includes(partValue)
? source.pattern
: '*'.repeat(length)
if (isSensibleSource) {
valueParts.push({ redacted: true, source: sourceIndex, pattern: placeholder })
} else {
let _value = partValue
const dedupedSourceRedactionContexts = []
for (const _sourceRedactionContext of sourceRedactionContext) {
const isPresentInDeduped = dedupedSourceRedactionContexts.some(_dedupedSourceRedactionContext =>
_dedupedSourceRedactionContext.start === _sourceRedactionContext.start &&
_dedupedSourceRedactionContext.end === _sourceRedactionContext.end
)
if (!isPresentInDeduped) {
dedupedSourceRedactionContexts.push(_sourceRedactionContext)
}
}
let offset = 0
for (const _sourceRedactionContext of dedupedSourceRedactionContexts) {
if (_sourceRedactionContext.start > 0) {
valueParts.push({
source: sourceIndex,
value: _value.substring(0, _sourceRedactionContext.start - offset),
})
_value = _value.substring(_sourceRedactionContext.start - offset)
offset = _sourceRedactionContext.start
}
const sensitive =
_value.substring(_sourceRedactionContext.start - offset, _sourceRedactionContext.end - offset)
const indexOfPartValueInPattern = source.value.indexOf(sensitive)
const pattern = indexOfPartValueInPattern === -1
? placeholder.substring(_sourceRedactionContext.start, _sourceRedactionContext.end)
: placeholder.substring(indexOfPartValueInPattern, indexOfPartValueInPattern + sensitive.length)
valueParts.push({
redacted: true,
source: sourceIndex,
pattern,
})
_value = _value.slice(pattern.length)
offset += pattern.length
}
if (_value.length) {
valueParts.push({
source: sourceIndex,
value: _value,
})
}
}
}
}
setRedactionPatterns (redactionNamePattern, redactionValuePattern) {
if (redactionNamePattern) {
try {
this._namePattern = new RegExp(redactionNamePattern, 'gmi')
} catch {
log.warn('[ASM] Redaction name pattern is not valid')
}
}
if (redactionValuePattern) {
try {
this._valuePattern = new RegExp(redactionValuePattern, 'gmi')
} catch {
log.warn('[ASM] Redaction value pattern is not valid')
}
}
}
}
module.exports = new SensitiveHandler()