dd-trace
Version:
Datadog APM tracing client for JavaScript
332 lines (305 loc) • 10.7 kB
JavaScript
const REDACTED = '<redacted>'
const MAX_SANITIZE_DEPTH = 128
const TRUNCATED_NESTING = '[Truncated: nesting exceeds redaction limit]'
const SECRET_NAME_SOURCE = [
'API_?KEY',
'APP_?KEY',
'TOKEN',
'SECRET',
'PASSWORD',
'PASSPHRASE',
'CREDENTIAL',
'PRIVATE_?KEY',
'CLIENT_?SECRET',
'ACCESS_?KEY',
'COOKIE',
].join('|')
const EXACT_SECRET_ASSIGNMENT_NAME_SOURCE = [
SECRET_NAME_SOURCE,
'AUTH',
'AUTHORIZATION',
'PASS',
'SET-COOKIE',
'PAT',
'JWT',
'WEBHOOK(?:_URL)?',
].join('|')
const SECRET_NAME_CHARS = '[A-Za-z0-9_.-]'
const SECRET_ASSIGNMENT_NAME_SOURCE = [
`(?:${EXACT_SECRET_ASSIGNMENT_NAME_SOURCE})`,
`[A-Za-z_]${SECRET_NAME_CHARS}*(?:${SECRET_NAME_SOURCE})${SECRET_NAME_CHARS}*`,
`[A-Za-z_]${SECRET_NAME_CHARS}*[-_]PASS`,
`[A-Za-z_]${SECRET_NAME_CHARS}*[-_]AUTH(?:ORIZATION)?`,
`[A-Za-z_]${SECRET_NAME_CHARS}*[-_](?:PAT|JWT|WEBHOOK(?:_URL)?)`,
'PASS',
'AUTH',
'AUTHORIZATION',
'COOKIE',
'SET-COOKIE',
].join('|')
const SECRET_FLAG_SOURCE = [
'api-key',
'app-key',
'token',
'secret',
'password',
'pass',
'credential',
'private-key',
'client-secret',
'access-key',
'auth',
].join('|')
const SECRET_VALUE_SOURCE = String.raw`("[^"]*"|'[^']*'|[^\s,;]+)`
const SENSITIVE_NAME_PATTERN = new RegExp(`(?:${SECRET_NAME_SOURCE})`, 'i')
const SENSITIVE_AUTH_NAME_PATTERN = /(?:^|_)AUTH(?:ORIZATION)?(?:_|$)/i
const SENSITIVE_COOKIE_NAME_PATTERN = /(?:^|_)SET_?COOKIE(?:_|$)|(?:^|_)COOKIE(?:_|$)/i
const SENSITIVE_PASS_NAME_PATTERN = /(?:^|_)PASS(?:_|$)/i
const SENSITIVE_TOKEN_ALIAS_NAME_PATTERN = /(?:^|_)(?:PAT|JWT|WEBHOOK(?:_URL)?)(?:_|$)/i
const SECRET_ASSIGNMENT_PATTERN = new RegExp(
String.raw`\b(${SECRET_ASSIGNMENT_NAME_SOURCE})\s*=\s*` + SECRET_VALUE_SOURCE,
'gi'
)
const SECRET_FLAG_PATTERN = new RegExp(
String.raw`(--(?:${SECRET_FLAG_SOURCE})(?:-[A-Za-z0-9]+)*)(=|\s+)` + SECRET_VALUE_SOURCE,
'gi'
)
const SECRET_FLAG_NAME_PATTERN = new RegExp(
`^--(?:${SECRET_FLAG_SOURCE})(?:-[A-Za-z0-9]+)*$`,
'i'
)
const AUTH_HEADER_PATTERN = /\b(Bearer)\s+([^\s'",}\]]+)/gi
const AUTH_SCHEME_ASSIGNMENT_QUOTED_PATTERN = new RegExp(
String.raw`\b(${SECRET_ASSIGNMENT_NAME_SOURCE})\s*=\s*(["'])(?:Bearer|Basic)\s+.*?\2`,
'gi'
)
const AUTH_SCHEME_ASSIGNMENT_PATTERN = new RegExp(
String.raw`\b(${SECRET_ASSIGNMENT_NAME_SOURCE})\s*=\s*(?:Bearer|Basic)\s+[^\s,;]+`,
'gi'
)
const DEFAULT_IGNORABLE_PATTERN = /\p{Default_Ignorable_Code_Point}/gu
const DEFAULT_IGNORABLE_TEST_PATTERN = /\p{Default_Ignorable_Code_Point}/u
const CONTROL_CHARACTER_TEST_PATTERN = /\p{Cc}/u
const UNSAFE_CONTROL_SOURCE = String.raw`[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]`
const UNSAFE_CONTROL_PATTERN = new RegExp(UNSAFE_CONTROL_SOURCE, 'g')
const UNSAFE_CONTROL_TEST_PATTERN = new RegExp(UNSAFE_CONTROL_SOURCE)
const PRIVATE_KEY_BLOCK_PATTERN =
/-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g
const JWT_VALUE_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
const KNOWN_TOKEN_VALUE_PATTERN =
/\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,})\b/g
const SECRET_HEADER_ENV_NAME_SOURCE = [
`(?:${SECRET_NAME_SOURCE}|PAT|JWT|WEBHOOK(?:_URL)?)`,
`[A-Za-z_]${SECRET_NAME_CHARS}*(?:${SECRET_NAME_SOURCE})${SECRET_NAME_CHARS}*`,
`[A-Za-z_]${SECRET_NAME_CHARS}*[-_]AUTH(?:ORIZATION)?`,
`[A-Za-z_]${SECRET_NAME_CHARS}*[-_](?:PAT|JWT|WEBHOOK(?:_URL)?)`,
].join('|')
const SECRET_HEADER_NAME_SOURCE = [
'dd-api-key',
'x-api-key',
'api-key',
'authorization',
'proxy-authorization',
'token',
'cookie',
'set-cookie',
SECRET_HEADER_ENV_NAME_SOURCE,
].join('|')
const SECRET_HEADER_PATTERN = new RegExp(
String.raw`\b((?:${SECRET_HEADER_NAME_SOURCE}))\s*:\s*("[^"]*"|'[^']*'|[^\r\n,}]+)`,
'gi'
)
const URL_CREDENTIAL_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)([^@\s/]+)(@)/gi
const GITHUB_SECRET_REFERENCE_PATTERN =
/(?:\b[A-Za-z_][A-Za-z0-9_.-]*\s*[:=]\s*)?\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g
const SAFE_REFERENCE_MARKER_PATTERN = /DDCIVARREF([0-9]+)X/g
const ENV_CONTAINER_KEYS = new Set([
'safeEnv',
'workflowEnv',
'jobEnv',
'stepEnv',
'inheritedEnv',
])
const SECRET_NAME_ONLY_KEYS = new Set([
'requiredEnvVars',
'requiredSecretEnvVars',
'secretEnvVars',
'missingEnvVars',
'originalSecretEnvVars',
])
/**
* Redacts secret-like values from arbitrary report data before it is serialized.
*
* @param {unknown} value data to sanitize
* @returns {unknown} sanitized copy
*/
function sanitizeForReport (value) {
return sanitizeValue(value, new WeakSet(), undefined, 0)
}
/**
* Redacts secret-like values from an environment variable map.
*
* @param {object|undefined} env environment map
* @returns {object|undefined} sanitized environment map
*/
function sanitizeEnv (env) {
if (!env || typeof env !== 'object' || Array.isArray(env)) return
const sanitized = {}
for (const [name, value] of Object.entries(env)) {
sanitized[name] = sanitizeEnvValue(name, value)
}
if (Object.keys(sanitized).length > 0) return sanitized
}
/**
* Redacts a single environment value when its name is secret-like.
*
* @param {string} name environment variable name
* @param {unknown} value environment variable value
* @returns {string|undefined} sanitized environment variable value
*/
function sanitizeEnvValue (name, value) {
if (isSensitiveName(name)) return REDACTED
if (value === undefined) return
return sanitizeString(String(value))
}
/**
* Redacts common inline secret forms from strings.
*
* @param {string} value string to sanitize
* @returns {string} sanitized string
*/
function sanitizeString (value) {
const references = []
const protectedValue = value.replaceAll(GITHUB_SECRET_REFERENCE_PATTERN, reference => {
return `DDCIVARREF${references.push(reference) - 1}X`
})
const sanitized = protectedValue
.replaceAll(DEFAULT_IGNORABLE_PATTERN, '')
.replaceAll(UNSAFE_CONTROL_PATTERN, '')
.replaceAll(PRIVATE_KEY_BLOCK_PATTERN, '<redacted-private-key>')
.replaceAll(JWT_VALUE_PATTERN, REDACTED)
.replaceAll(KNOWN_TOKEN_VALUE_PATTERN, REDACTED)
.replaceAll(AUTH_SCHEME_ASSIGNMENT_QUOTED_PATTERN, `$1=${REDACTED}`)
.replaceAll(AUTH_SCHEME_ASSIGNMENT_PATTERN, `$1=${REDACTED}`)
.replaceAll(SECRET_ASSIGNMENT_PATTERN, `$1=${REDACTED}`)
.replaceAll(SECRET_FLAG_PATTERN, `$1$2${REDACTED}`)
.replaceAll(SECRET_HEADER_PATTERN, `$1: ${REDACTED}`)
.replaceAll(AUTH_HEADER_PATTERN, `$1 ${REDACTED}`)
.replaceAll(URL_CREDENTIAL_PATTERN, `$1${REDACTED}$3`)
return sanitized.replaceAll(SAFE_REFERENCE_MARKER_PATTERN, (marker, index) => references[Number(index)] || marker)
}
/**
* Detects default-ignorable Unicode characters that can conceal executable text or secret names.
*
* @param {unknown} value candidate text
* @returns {boolean} true when the text contains a default-ignorable Unicode character
*/
function hasUnicodeDefaultIgnorable (value) {
return DEFAULT_IGNORABLE_TEST_PATTERN.test(String(value ?? ''))
}
function hasUnsafeInvisibleCharacter (value) {
const text = String(value ?? '')
return hasUnicodeDefaultIgnorable(text) || UNSAFE_CONTROL_TEST_PATTERN.test(text)
}
function hasUnsafeExecutionCharacter (value) {
return hasUnicodeDefaultIgnorable(value) || CONTROL_CHARACTER_TEST_PATTERN.test(String(value ?? ''))
}
/**
* Makes untrusted text safe to print to an interactive terminal while preserving line breaks.
*
* @param {unknown} value console value
* @returns {string} inert console text
*/
function sanitizeConsoleText (value) {
let result = ''
for (const character of sanitizeString(String(value ?? ''))) {
const code = character.charCodeAt(0)
result += character === '\n' || character === '\t' || code > 0x1F && code !== 0x7F
? character
: String.raw`\u${code.toString(16).padStart(4, '0')}`
}
return result
}
/**
* Returns whether a key or variable name usually carries secret values.
*
* @param {string} name key or environment variable name
* @returns {boolean} true when values under this name should be redacted
*/
function isSensitiveName (name) {
const normalized = String(name || '').replaceAll(/[-.]/g, '_')
return SENSITIVE_NAME_PATTERN.test(normalized) ||
SENSITIVE_AUTH_NAME_PATTERN.test(normalized) ||
SENSITIVE_COOKIE_NAME_PATTERN.test(normalized) ||
SENSITIVE_PASS_NAME_PATTERN.test(normalized) ||
SENSITIVE_TOKEN_ALIAS_NAME_PATTERN.test(normalized)
}
function sanitizeValue (value, seen, key, depth) {
if (typeof value === 'string') {
if (key && isSensitiveName(key) && !SECRET_NAME_ONLY_KEYS.has(key)) return REDACTED
return sanitizeString(value)
}
if (value === null || typeof value !== 'object') return value
if (depth > MAX_SANITIZE_DEPTH) return TRUNCATED_NESTING
if (seen.has(value)) return '[Circular]'
seen.add(value)
if (Array.isArray(value)) {
const sanitized = sanitizeArray(value, seen, key, depth)
seen.delete(value)
return sanitized
}
if (key && ENV_CONTAINER_KEYS.has(key)) {
const env = sanitizeEnv(value)
seen.delete(value)
return env || {}
}
const sanitized = {}
for (const [entryKey, entryValue] of Object.entries(value)) {
if (isSensitiveName(entryKey) && !SECRET_NAME_ONLY_KEYS.has(entryKey)) {
sanitized[entryKey] = REDACTED
continue
}
sanitized[entryKey] = sanitizeValue(entryValue, seen, entryKey, depth + 1)
}
seen.delete(value)
return sanitized
}
/**
* Redacts secret flag/value pairs in arrays such as argv before sanitizing nested values.
*
* @param {Array<unknown>} value array to sanitize
* @param {WeakSet<object>} seen objects currently being sanitized
* @param {string|undefined} key parent key
* @param {number} depth current nesting depth
* @returns {Array<unknown>} sanitized array
*/
function sanitizeArray (value, seen, key, depth) {
const sanitized = []
for (let index = 0; index < value.length; index++) {
const item = value[index]
if (typeof item === 'string' && SECRET_FLAG_NAME_PATTERN.test(item) && index + 1 < value.length) {
sanitized.push(sanitizeString(item))
if (!isFlagToken(value[index + 1])) {
sanitized.push(REDACTED)
index++
}
continue
}
sanitized.push(sanitizeValue(item, seen, key, depth + 1))
}
return sanitized
}
function isFlagToken (value) {
return typeof value === 'string' && /^-{1,2}\S/.test(value)
}
module.exports = {
hasUnsafeExecutionCharacter,
hasUnsafeInvisibleCharacter,
isSensitiveName,
sanitizeConsoleText,
sanitizeEnv,
sanitizeEnvValue,
sanitizeForReport,
sanitizeString,
}