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 1 commit
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
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 @@ -26,5 +26,6 @@ module.exports = {
pgPoolQueryStart: dc.channel('datadog:pg:pool:query:start'),
mysql2OuterQueryStart: dc.channel('datadog:mysql2:outerquery:start'),
wafRunFinished: dc.channel('datadog:waf:run:finish'),
fsOperationStart: dc.channel('apm:fs:operation:start')
fsOperationStart: dc.channel('apm:fs:operation:start'),
expressMiddlewareError: dc.channel('apm:express:middleware:error')
}
31 changes: 21 additions & 10 deletions packages/dd-trace/src/appsec/rasp/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'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')
Expand Down Expand Up @@ -31,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 @@ -83,12 +79,25 @@ 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 () {
Expand All @@ -97,10 +106,12 @@ function 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
}
65 changes: 65 additions & 0 deletions packages/dd-trace/test/appsec/rasp/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
'use strict'

const proxyquire = require('proxyquire')
const { expressMiddlewareError } = require('../../../src/appsec/channels')
const rasp = require('../../../src/appsec/rasp')
const { handleUncaughtExceptionMonitor } = require('../../../src/appsec/rasp')
const { DatadogRaspAbortError } = require('../../../src/appsec/rasp/utils')

describe('RASP', () => {
let onErrorSub, onErrorUnsub, block

beforeEach(() => {
const config = {
appsec: {
Expand All @@ -15,6 +20,9 @@ describe('RASP', () => {
}
}

onErrorSub = sinon.spy(expressMiddlewareError, 'subscribe')
onErrorUnsub = sinon.spy(expressMiddlewareError, 'unsubscribe')

rasp.enable(config)
})

Expand All @@ -31,4 +39,61 @@ describe('RASP', () => {
handleUncaughtExceptionMonitor(err)
})
})

describe('enable/disable', () => {
it('should subscribe to apm:express:middleware:error', () => {
sinon.assert.calledOnce(onErrorSub)
})

it('should unsubscribe to apm:express:middleware:error', () => {
rasp.disable()

sinon.assert.calledOnce(onErrorUnsub)
})
})

describe('blockOnDatadogRaspAbortError', () => {
let rasp, req, res, blockingAction, blocked

beforeEach(() => {
req = {}
res = {}
blockingAction = 'block'
block = sinon.stub()

rasp = proxyquire('../../../src/appsec/rasp', {
'../blocking': {
block,
isBlocked: sinon.stub().callsFake(() => blocked)
}
})
})

afterEach(() => {
sinon.restore()
})

it('should skip non DatadogRaspAbortError', () => {
rasp.blockOnDatadogRaspAbortError({ error: new Error() })

sinon.assert.notCalled(block)
})

it('should block DatadogRaspAbortError first time', () => {
rasp.blockOnDatadogRaspAbortError({ error: new DatadogRaspAbortError(req, res, blockingAction) })

sinon.assert.calledOnce(block)
})

it('should skip calling block if blocked before', () => {
rasp.blockOnDatadogRaspAbortError({ error: new DatadogRaspAbortError(req, res, blockingAction) })

blocked = true

rasp.blockOnDatadogRaspAbortError({ error: new DatadogRaspAbortError(req, res, blockingAction) })
rasp.blockOnDatadogRaspAbortError({ error: new DatadogRaspAbortError(req, res, blockingAction) })

sinon.assert.calledOnce(block)
})
})
})
30 changes: 20 additions & 10 deletions packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,28 @@ describe('RASP - lfi', () => {
}
}

function getAppSync (fn, args, options) {
return (req, res) => {
try {
const result = fn(args)
options.onfinish?.(result)
} catch (e) {
if (e.message === 'DatadogRaspAbortError') {
uurien marked this conversation as resolved.
Show resolved Hide resolved
res.writeHead(418)
}
}
res.end('end')
}
}

function runFsMethodTest (description, options, fn, ...args) {
const { vulnerableIndex = 0, ruleEvalCount } = options

describe(description, () => {
const getAppFn = options.getAppFn ?? getApp

it('should block param from the request', async () => {
app = getApp(fn, args, options)
app = getAppFn(fn, args, options)

const file = args[vulnerableIndex]
return testBlockingRequest(`/?file=${file}`, undefined, ruleEvalCount)
Expand All @@ -97,7 +113,7 @@ describe('RASP - lfi', () => {
})

it('should not block if param not found in the request', async () => {
app = getApp(fn, args, options)
app = getAppFn(fn, args, options)

await axios.get('/?file=/test.file')

Expand All @@ -113,14 +129,8 @@ describe('RASP - lfi', () => {
desc += ` with vulnerable index ${vulnerableIndex}`
}
describe(desc, () => {
runFsMethodTest(`test fs.${methodName}Sync method`, options, (args) => {
return new Promise((resolve, reject) => {
try {
resolve(require('fs')[`${methodName}Sync`](...args))
} catch (e) {
reject(e)
}
})
runFsMethodTest(`test fs.${methodName}Sync method`, { ...options, getAppFn: getAppSync }, (args) => {
return require('fs')[`${methodName}Sync`](...args)
}, ...args)

runFsMethodTest(`test fs.${methodName} method`, options, (args) => {
Expand Down
Loading