Skip to content

Commit

Permalink
rasp lfi and iast using rasp fs-plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
iunanua committed Sep 12, 2024
1 parent 14ebf97 commit b1efbeb
Show file tree
Hide file tree
Showing 14 changed files with 652 additions and 4 deletions.
24 changes: 24 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,36 @@ 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,
callback // TODO: callback should be included or excluded in the start-end lifetime?
},
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
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,13 @@ 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
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
65 changes: 65 additions & 0 deletions packages/dd-trace/src/appsec/rasp/lfi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict'

const { fsOperationStart } = require('../channels')
const { storage } = require('../../../../datadog-core')
const web = require('../../plugins/util/web')
const { enable: enableFsPlugin, disable: disableFsPlugin } = require('./fs-plugin')
const addresses = require('../addresses')
const waf = require('../waf')
const { RULE_TYPES, handleResult } = require('./utils')
const { block } = require('../blocking')
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 path = ctx?.path
if (!path) return

const store = storage.getStore()
if (!store) return

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

if (fs.root && !fs.opExcluded && shouldAnalyze(path)) {
const persistent = {
[addresses.FS_OPERATION_PATH]: path
}

const result = waf.run({ persistent }, req, RULE_TYPES.LFI)

if (result) {
const abortController = new AbortController()
handleResult(result, req, res, abortController, config)

const { aborted, reason } = abortController.signal
if (aborted) {
block(req, res, web.root(req), null, reason.blockingAction)
}
}
}
}

function shouldAnalyze (path) {
return isAbsolute(path) || path.contains('../')
}

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 @@ -45,6 +45,14 @@ const InjectionAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/inje
})

describe('path-traversal-analyzer', () => {
before(() => {
pathTraversalAnalyzer.enable()
})

after(() => {
pathTraversalAnalyzer.disable()
})

it('Analyzer should be subscribed to proper channel', () => {
expect(pathTraversalAnalyzer._subscriptions).to.have.lengthOf(1)
expect(pathTraversalAnalyzer._subscriptions[0]._channel.name).to.equals('apm:fs:operation:start')
Expand Down
23 changes: 22 additions & 1 deletion packages/dd-trace/test/appsec/iast/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ describe('IAST Index', () => {
let mockVulnerabilityReporter
let mockIast
let mockOverheadController
let appsecFsPlugin

const config = new Config({
experimental: {
Expand All @@ -125,9 +126,14 @@ describe('IAST Index', () => {
startGlobalContext: sinon.stub(),
finishGlobalContext: sinon.stub()
}
appsecFsPlugin = {
enable: sinon.stub(),
disable: sinon.stub()
}
mockIast = proxyquire('../../../src/appsec/iast', {
'./vulnerability-reporter': mockVulnerabilityReporter,
'./overhead-controller': mockOverheadController
'./overhead-controller': mockOverheadController,
'../rasp/fs-plugin': appsecFsPlugin
})
})

Expand All @@ -136,6 +142,21 @@ describe('IAST Index', () => {
mockIast.disable()
})

describe('enable', () => {
it('should enable AppsecFsPlugin', () => {
mockIast.enable(config)
expect(appsecFsPlugin.enable).to.have.been.calledOnceWithExactly('iast')
})
})

describe('disable', () => {
it('should disable AppsecFsPlugin', () => {
mockIast.enable(config)
mockIast.disable()
expect(appsecFsPlugin.disable).to.have.been.calledOnceWithExactly('iast')
})
})

describe('managing overhead controller global context', () => {
it('should start global context refresher on iast enabled', () => {
mockIast.enable(config)
Expand Down
Loading

0 comments on commit b1efbeb

Please sign in to comment.