UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

295 lines (249 loc) 10.9 kB
'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) 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 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()