UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

143 lines (117 loc) 4.22 kB
'use strict' const web = require('../../plugins/util/web') const { setUncaughtExceptionCaptureCallbackStart, expressMiddlewareError, fastifyMiddlewareError, routerMiddlewareError } = require('../channels') const { block, registerBlockDelegation, isBlocked } = require('../blocking') const { updateRaspRuleMatchMetricTags } = require('../telemetry') const ssrf = require('./ssrf') const sqli = require('./sql_injection') const lfi = require('./lfi') const cmdi = require('./command_injection') const { DatadogRaspAbortError } = require('./utils') function removeAllListeners (emitter, event) { const listeners = emitter.listeners(event) emitter.removeAllListeners(event) let cleaned = false return function () { if (cleaned === true) { return } cleaned = true for (const listener of listeners) { emitter.on(event, listener) } } } function findDatadogRaspAbortError (err, deep = 10) { if (err instanceof DatadogRaspAbortError) { return err } if (err?.cause && deep > 0) { return findDatadogRaspAbortError(err.cause, deep - 1) } } function handleUncaughtExceptionMonitor (error) { if (!blockOnDatadogRaspAbortError({ error, isTopLevel: true })) return if (process.hasUncaughtExceptionCaptureCallback()) { // uncaughtException event is not executed when hasUncaughtExceptionCaptureCallback is true let previousCb const cb = ({ currentCallback, abortController }) => { setUncaughtExceptionCaptureCallbackStart.unsubscribe(cb) if (!currentCallback) { abortController.abort() return } previousCb = currentCallback } setUncaughtExceptionCaptureCallbackStart.subscribe(cb) process.setUncaughtExceptionCaptureCallback(null) // For some reason, previous callback was defined before the instrumentation // We can not restore it, so we let the app decide if (previousCb) { process.setUncaughtExceptionCaptureCallback(() => { process.setUncaughtExceptionCaptureCallback(null) process.setUncaughtExceptionCaptureCallback(previousCb) }) } } else { const cleanUp = removeAllListeners(process, 'uncaughtException') const handler = () => { process.removeListener('uncaughtException', handler) } setTimeout(() => { process.removeListener('uncaughtException', handler) cleanUp() }) process.on('uncaughtException', handler) } } function blockOnDatadogRaspAbortError ({ error, isTopLevel }) { const abortError = findDatadogRaspAbortError(error) if (!abortError) return false const { req, res, blockingAction, raspRule, ruleTriggered } = abortError if (!isBlocked(res)) { const blockFn = isTopLevel ? block : registerBlockDelegation const blocked = blockFn(req, res, web.root(req), null, blockingAction) if (ruleTriggered) { // block() returns a bool, and registerBlockDelegation() returns a promise // we use Promise.resolve() to handle both cases Promise.resolve(blocked).then(blocked => { // TODO: bug: this metric is not called when the raspAbortError is caught by user // or on subsequent blockDelegations updateRaspRuleMatchMetricTags(req, raspRule, true, blocked) }) } } return true } function enable (config) { ssrf.enable(config) sqli.enable(config) lfi.enable(config) cmdi.enable(config) process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor) expressMiddlewareError.subscribe(blockOnDatadogRaspAbortError) fastifyMiddlewareError.subscribe(blockOnDatadogRaspAbortError) routerMiddlewareError.subscribe(blockOnDatadogRaspAbortError) } function disable () { ssrf.disable() sqli.disable() lfi.disable() cmdi.disable() process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor) expressMiddlewareError.unsubscribe(blockOnDatadogRaspAbortError) fastifyMiddlewareError.unsubscribe(blockOnDatadogRaspAbortError) routerMiddlewareError.unsubscribe(blockOnDatadogRaspAbortError) } module.exports = { enable, disable, handleUncaughtExceptionMonitor, // exported only for testing purpose blockOnDatadogRaspAbortError // exported only for testing purpose }