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

Exploit Prevention LFI #4676

Merged
merged 36 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e626f7b
rasp lfi and iast using rasp fs-plugin
iunanua Sep 12, 2024
93222b9
Add rasp lfi capability in RC
iunanua Sep 12, 2024
fe785ca
Handle aborted operations in fs instrumentation
iunanua Sep 16, 2024
2845167
enable test without express
iunanua Sep 16, 2024
d16ab3f
cleanup and console log to debug test error
iunanua Sep 17, 2024
0eded75
Do not throw
iunanua Sep 17, 2024
7cb0756
another test
iunanua Sep 17, 2024
841a059
Try increasing timeout
iunanua Sep 17, 2024
7e9a431
Enable debug again
iunanua Sep 17, 2024
12378b2
Enable debug again
iunanua Sep 17, 2024
e4e3ea5
increase timeout a lot
iunanua Sep 17, 2024
1742560
increase timeout more
iunanua Sep 17, 2024
87ea5b4
New lfi test
iunanua Sep 17, 2024
13c2712
Increase test timeout
iunanua Sep 17, 2024
08a26b6
print all errors
iunanua Sep 17, 2024
a0646f9
remote debug info
iunanua Sep 17, 2024
2bdd900
Handle the different invocation cases
iunanua Sep 17, 2024
694954d
Handle non string properties
iunanua Sep 17, 2024
1f0f1a8
specify types to be analyzed
iunanua Sep 17, 2024
fbc9ae6
a bunch of tests
iunanua Sep 18, 2024
f6235dd
Merge branch 'master' into igor/lfi-exploit-prevention
iunanua Sep 20, 2024
06b6a24
clean up
iunanua Sep 19, 2024
72510cc
Merge branch 'master' into igor/lfi-exploit-prevention
iunanua Sep 23, 2024
467d916
rasp lfi subs delayed (#4715)
iunanua Sep 30, 2024
05b07a1
Use a constant
iunanua Sep 30, 2024
dd5a037
Do not enable rasp in some tests
iunanua Sep 30, 2024
ab9b9d4
Remove not needed config property
iunanua Oct 1, 2024
bbd3d0c
Rename properties
iunanua Oct 1, 2024
eac015b
Test iast and rasp fs-plugin subscription order
iunanua Oct 1, 2024
c38edf9
Avoid multiple analyzeLfi subscriptions
iunanua Oct 3, 2024
313e410
Merge branch 'master' into igor/lfi-exploit-prevention
iunanua Oct 4, 2024
e49e10c
Block synchronous operations
iunanua Oct 7, 2024
305cbeb
Include synchronous blocking integration test
iunanua Oct 7, 2024
719a487
Test refactor
iunanua Oct 7, 2024
d6ffbb1
rename test file
iunanua Oct 7, 2024
8efbd70
Cleanup
iunanua Oct 7, 2024
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'
}
4 changes: 3 additions & 1 deletion packages/dd-trace/src/appsec/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ module.exports = {
pgQueryStart: dc.channel('apm:pg:query:start'),
pgPoolQueryStart: dc.channel('datadog:pg:pool:query:start'),
mysql2OuterQueryStart: dc.channel('datadog:mysql2:outerquery:start'),
wafRunFinished: dc.channel('datadog:waf:run:finish')
wafRunFinished: dc.channel('datadog:waf:run:finish'),
fsOperationStart: dc.channel('apm:fs:operation:start'),
expressMiddlewareError: dc.channel('apm:express:middleware:error')
}
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
uurien marked this conversation as resolved.
Show resolved Hide resolved

// 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, IAST_MODULE } = 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_MODULE)
enableAllAnalyzers(config)
enableTaintTracking(config.iast, iastTelemetry.verbosity)
requestStart.subscribe(onIncomingHttpRequestStart)
Expand All @@ -44,6 +46,7 @@ function disable () {
isEnabled = false

iastTelemetry.stop()
disableFsPlugin(IAST_MODULE)
disableAllAnalyzers()
disableTaintTracking()
overheadController.finishGlobalContext()
Expand Down
99 changes: 99 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,99 @@
'use strict'

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

const RASP_MODULE = 'rasp'
const IAST_MODULE = 'iast'

const enabledFor = {
[RASP_MODULE]: false,
[IAST_MODULE]: false
}

let fsPlugin

function enterWith (fsProps, store = storage.getStore()) {
if (store && !store.fs?.opExcluded) {
CarlesDD marked this conversation as resolved.
Show resolved Hide resolved
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,

RASP_MODULE,
IAST_MODULE
}
34 changes: 24 additions & 10 deletions packages/dd-trace/src/appsec/rasp/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict'

const web = require('../../plugins/util/web')
const { setUncaughtExceptionCaptureCallbackStart } = require('../channels')
const { block } = require('../blocking')
const { setUncaughtExceptionCaptureCallbackStart, expressMiddlewareError } = require('../channels')
const { block, isBlocked } = require('../blocking')
const ssrf = require('./ssrf')
const sqli = require('./sql_injection')
const lfi = require('./lfi')

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

Expand All @@ -30,17 +31,13 @@ function findDatadogRaspAbortError (err, deep = 10) {
return err
}

if (err.cause && deep > 0) {
if (err?.cause && deep > 0) {
return findDatadogRaspAbortError(err.cause, deep - 1)
}
}

function handleUncaughtExceptionMonitor (err) {
const abortError = findDatadogRaspAbortError(err)
if (!abortError) return

const { req, res, blockingAction } = abortError
block(req, res, web.root(req), null, blockingAction)
function handleUncaughtExceptionMonitor (error) {
if (!blockOnDatadogRaspAbortError({ error })) return

if (!process.hasUncaughtExceptionCaptureCallback()) {
const cleanUp = removeAllListeners(process, 'uncaughtException')
Expand Down Expand Up @@ -82,22 +79,39 @@ function handleUncaughtExceptionMonitor (err) {
}
}

function blockOnDatadogRaspAbortError ({ error }) {
const abortError = findDatadogRaspAbortError(error)
if (!abortError) return false

const { req, res, blockingAction } = abortError
if (!isBlocked(res)) {
block(req, res, web.root(req), null, blockingAction)
}

return true
}

function enable (config) {
ssrf.enable(config)
sqli.enable(config)
lfi.enable(config)

process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
expressMiddlewareError.subscribe(blockOnDatadogRaspAbortError)
}

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

process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
if (expressMiddlewareError.hasSubscribers) expressMiddlewareError.unsubscribe(blockOnDatadogRaspAbortError)
}

module.exports = {
enable,
disable,
handleUncaughtExceptionMonitor // exported only for testing purpose
handleUncaughtExceptionMonitor, // exported only for testing purpose
blockOnDatadogRaspAbortError // exported only for testing purpose
}
Loading
Loading