Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rasp lfi #4676

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/datadog-instrumentations/src/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { createWrapRouterMethod } = require('./router')
const shimmer = require('../../datadog-shimmer')
const { addHook, channel } = require('./helpers/instrument')
const tracingChannel = require('dc-polyfill').tracingChannel

const handleChannel = channel('apm:express:request:handle')

Expand Down Expand Up @@ -35,13 +36,35 @@ function wrapResponseJson (json) {
}
}

const responseRenderChannel = tracingChannel('datadog:express:response:render')

function wrapResponseRender (render) {
return function wrappedRender (view, options, callback) {
if (!responseRenderChannel.start.hasSubscribers) {
return render.apply(this, arguments)
}

return responseRenderChannel.traceSync(
render,
{
req: this.req,
view,
options
},
this,
...arguments
)
}
}

addHook({ name: 'express', versions: ['>=4'] }, express => {
shimmer.wrap(express.application, 'handle', wrapHandle)
shimmer.wrap(express.Router, 'use', wrapRouterMethod)
shimmer.wrap(express.Router, 'route', wrapRouterMethod)

shimmer.wrap(express.response, 'json', wrapResponseJson)
shimmer.wrap(express.response, 'jsonp', wrapResponseJson)
shimmer.wrap(express.response, 'render', wrapResponseRender)

return express
})
Expand Down
34 changes: 27 additions & 7 deletions packages/datadog-instrumentations/src/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,24 +266,44 @@ function createWrapFunction (prefix = '', override = '') {
const lastIndex = arguments.length - 1
const cb = typeof arguments[lastIndex] === 'function' && arguments[lastIndex]
const innerResource = new AsyncResource('bound-anonymous-fn')
const message = getMessage(method, getMethodParamsRelationByPrefix(prefix)[operation], arguments, this)
const params = getMethodParamsRelationByPrefix(prefix)[operation]
const abortController = new AbortController()
const message = { ...getMessage(method, params, arguments, this), abortController }

const finish = innerResource.bind(function (error) {
if (error !== null && typeof error === 'object') { // fs.exists receives a boolean
errorChannel.publish(error)
}
finishChannel.publish()
})

if (cb) {
const outerResource = new AsyncResource('bound-anonymous-fn')

arguments[lastIndex] = shimmer.wrapFunction(cb, cb => innerResource.bind(function (e) {
if (e !== null && typeof e === 'object') { // fs.exists receives a boolean
errorChannel.publish(e)
}

finishChannel.publish()

finish(e)
return outerResource.runInAsyncScope(() => cb.apply(this, arguments))
}))
}

return innerResource.runInAsyncScope(() => {
startChannel.publish(message)

if (abortController.signal.aborted) {
const error = abortController.signal.reason || new Error('Aborted')

if (prefix === 'promises.') {
finish(error)
return Promise.reject(error)
} else if (name.includes('Sync') || !cb) {
finish(error)
throw error
} else if (cb) {
arguments[lastIndex](error)
return
}
}

try {
const result = original.apply(this, arguments)
if (cb) return result
Expand Down
2 changes: 2 additions & 0 deletions packages/dd-trace/src/appsec/addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ module.exports = {
WAF_CONTEXT_PROCESSOR: 'waf.context.processor',

HTTP_OUTGOING_URL: 'server.io.net.url',
FS_OPERATION_PATH: 'server.io.fs.file',

DB_STATEMENT: 'server.db.statement',
DB_SYSTEM: 'server.db.system'
}
3 changes: 2 additions & 1 deletion packages/dd-trace/src/appsec/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ module.exports = {
setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start'),
pgQueryStart: dc.channel('apm:pg:query:start'),
pgPoolQueryStart: dc.channel('datadog:pg:pool:query:start'),
wafRunFinished: dc.channel('datadog:waf:run:finish')
wafRunFinished: dc.channel('datadog:waf:run:finish'),
fsOperationStart: dc.channel('apm:fs:operation:start')
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ class PathTraversalAnalyzer extends InjectionAnalyzer {

onConfigure () {
this.addSub('apm:fs:operation:start', (obj) => {
if (ignoredOperations.includes(obj.operation)) return
const store = storage.getStore()
const outOfReqOrChild = !store?.fs?.root

// we could filter out all the nested fs.operations based on store.fs.root
// but if we spect a store in the context to be present we are going to exclude
// all out_of_the_request fs.operations
// AppsecFsPlugin must be enabled
if (ignoredOperations.includes(obj.operation) || outOfReqOrChild) return

const pathArguments = []
if (obj.dest) {
Expand Down
3 changes: 3 additions & 0 deletions packages/dd-trace/src/appsec/iast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
} = require('./taint-tracking')
const { IAST_ENABLED_TAG_KEY } = require('./tags')
const iastTelemetry = require('./telemetry')
const { enable: enableFsPlugin, disable: disableFsPlugin } = require('../rasp/fs-plugin')

// TODO Change to `apm:http:server:request:[start|close]` when the subscription
// order of the callbacks can be enforce
Expand All @@ -27,6 +28,7 @@ function enable (config, _tracer) {
if (isEnabled) return

iastTelemetry.configure(config, config.iast?.telemetryVerbosity)
enableFsPlugin('iast')
enableAllAnalyzers(config)
enableTaintTracking(config.iast, iastTelemetry.verbosity)
requestStart.subscribe(onIncomingHttpRequestStart)
Expand All @@ -44,6 +46,7 @@ function disable () {
isEnabled = false

iastTelemetry.stop()
disableFsPlugin('iast')
disableAllAnalyzers()
disableTaintTracking()
overheadController.finishGlobalContext()
Expand Down
93 changes: 93 additions & 0 deletions packages/dd-trace/src/appsec/rasp/fs-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict'

const Plugin = require('../../plugins/plugin')
const { storage } = require('../../../../datadog-core')
const log = require('../../log')

const enabledFor = {
rasp: false,
iast: false
}

let fsPlugin

function enterWith (fsProps, store = storage.getStore()) {
if (store && !store.fs?.opExcluded) {
storage.enterWith({
...store,
fs: {
...store.fs,
...fsProps,
parentStore: store
}
})
}
}

class AppsecFsPlugin extends Plugin {
enable () {
this.addSub('apm:fs:operation:start', this._onFsOperationStart)
this.addSub('apm:fs:operation:finish', this._onFsOperationFinishOrRenderEnd)
this.addSub('tracing:datadog:express:response:render:start', this._onResponseRenderStart)
this.addSub('tracing:datadog:express:response:render:end', this._onFsOperationFinishOrRenderEnd)

super.configure(true)
}

disable () {
super.configure(false)
}

_onFsOperationStart () {
const store = storage.getStore()
if (store) {
enterWith({ root: store.fs?.root === undefined }, store)
}
}

_onResponseRenderStart () {
enterWith({ opExcluded: true })
}

_onFsOperationFinishOrRenderEnd () {
const store = storage.getStore()
if (store?.fs?.parentStore) {
storage.enterWith(store.fs.parentStore)
}
}
}

function enable (mod) {
if (enabledFor[mod] !== false) return

enabledFor[mod] = true

if (!fsPlugin) {
fsPlugin = new AppsecFsPlugin()
fsPlugin.enable()
}

log.info(`Enabled AppsecFsPlugin for ${mod}`)
}

function disable (mod) {
if (!mod || !enabledFor[mod]) return

enabledFor[mod] = false

const allDisabled = Object.values(enabledFor).every(val => val === false)
if (allDisabled) {
fsPlugin?.disable()

fsPlugin = undefined
}

log.info(`Disabled AppsecFsPlugin for ${mod}`)
}

module.exports = {
enable,
disable,

AppsecFsPlugin
}
3 changes: 3 additions & 0 deletions packages/dd-trace/src/appsec/rasp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { setUncaughtExceptionCaptureCallbackStart } = require('../channels')
const { block } = require('../blocking')
const ssrf = require('./ssrf')
const sqli = require('./sql_injection')
const lfi = require('./lfi')

const { DatadogRaspAbortError } = require('./utils')

Expand Down Expand Up @@ -85,13 +86,15 @@ function handleUncaughtExceptionMonitor (err) {
function enable (config) {
ssrf.enable(config)
sqli.enable(config)
lfi.enable(config)

process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
}

function disable () {
ssrf.disable()
sqli.disable()
lfi.disable()

process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
}
Expand Down
90 changes: 90 additions & 0 deletions packages/dd-trace/src/appsec/rasp/lfi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict'

const { fsOperationStart } = require('../channels')
const { storage } = require('../../../../datadog-core')
const { enable: enableFsPlugin, disable: disableFsPlugin } = require('./fs-plugin')
const { FS_OPERATION_PATH } = require('../addresses')
const waf = require('../waf')
const { RULE_TYPES, handleResult } = require('./utils')
const { isAbsolute } = require('path')

let config

function enable (_config) {
config = _config

enableFsPlugin('rasp')

fsOperationStart.subscribe(analyzeLfi)
}

function disable () {
if (fsOperationStart.hasSubscribers) fsOperationStart.unsubscribe(analyzeLfi)

disableFsPlugin('rasp')
}

function analyzeLfi (ctx) {
const store = storage.getStore()
if (!store) return

const { req, fs, res } = store
if (!req || !fs) return

getPaths(ctx, fs).forEach(path => {
const persistent = {
[FS_OPERATION_PATH]: path
}

const result = waf.run({ persistent }, req, RULE_TYPES.LFI)
handleResult(result, req, res, ctx.abortController, config)
})
}

function getPaths (ctx, fs) {
// these properties could have String, Buffer, URL, Integer or FileHandle types
const pathArguments = [
ctx.dest,
ctx.existingPath,
ctx.file,
ctx.newPath,
ctx.oldPath,
ctx.path,
ctx.prefix,
ctx.src,
ctx.target
]

return pathArguments
.map(path => pathToStr(path))
.filter(path => shouldAnalyze(path, fs))
}

function pathToStr (path) {
if (!path) return

if (typeof path === 'string' ||
path instanceof String ||
path instanceof Buffer ||
path instanceof URL) {
return path.toString()
}
}

function shouldAnalyze (path, fs) {
if (!path) return

const notExcludedRootOp = !fs.opExcluded && fs.root
return notExcludedRootOp && (isAbsolute(path) || path.includes('../') || shouldAnalyzeURLFile(path, fs))
}

function shouldAnalyzeURLFile (path, fs) {
if (path.startsWith('file://')) {
return shouldAnalyze(path.substring(7), fs)
}
}

module.exports = {
enable,
disable
}
3 changes: 2 additions & 1 deletion packages/dd-trace/src/appsec/rasp/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ if (abortOnUncaughtException) {

const RULE_TYPES = {
SSRF: 'ssrf',
SQL_INJECTION: 'sql_injection'
SQL_INJECTION: 'sql_injection',
LFI: 'lfi'
}

class DatadogRaspAbortError extends Error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ module.exports = {
APM_TRACING_ENABLED: 1n << 19n,
ASM_RASP_SQLI: 1n << 21n,
ASM_RASP_SSRF: 1n << 23n,
ASM_RASP_LFI: 1n << 24n,
APM_TRACING_SAMPLE_RULES: 1n << 29n
}
2 changes: 2 additions & 0 deletions packages/dd-trace/src/appsec/remote_config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function enableWafUpdate (appsecConfig) {
if (appsecConfig.rasp?.enabled) {
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, true)
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, true)
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, true)
}

// TODO: delete noop handlers and kPreUpdate and replace with batched handlers
Expand Down Expand Up @@ -106,6 +107,7 @@ function disableWafUpdate () {

rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, false)
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false)
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, false)

rc.removeProductHandler('ASM_DATA')
rc.removeProductHandler('ASM_DD')
Expand Down
Loading
Loading