Skip to content

Fix fs and rasp/iast problems #6088

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

Merged
merged 12 commits into from
Jul 16, 2025
1 change: 0 additions & 1 deletion packages/datadog-instrumentations/src/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,6 @@ function createWrapFunction (prefix = '', override = '') {
}
)
}

finishChannel.runStores(ctx, () => {})

return result
Expand Down
25 changes: 14 additions & 11 deletions packages/dd-trace/src/appsec/rasp/fs-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,27 @@ const enabledFor = {

let fsPlugin

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

return 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)
this.addBind('apm:fs:operation:start', this._onFsOperationStart)
this.addBind('apm:fs:operation:finish', this._onFsOperationFinishOrRenderEnd)
this.addBind('tracing:datadog:express:response:render:start', this._onResponseRenderStart)
this.addBind('tracing:datadog:express:response:render:end', this._onFsOperationFinishOrRenderEnd)

super.configure(true)
}
Expand All @@ -44,19 +46,20 @@ class AppsecFsPlugin extends Plugin {
_onFsOperationStart () {
const store = storage('legacy').getStore()
if (store) {
enterWith({ root: store.fs?.root === undefined }, store)
return getStoreToStart({ root: store.fs?.root === undefined }, store)
}
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,26 @@ prepareTestServerForIast('integration test', (testThatRequestHasVulnerability, t

describe('test stat', () => {
runFsMethodTestThreeWay('stat', 0, null, __filename)

describe('with two calls to async method without waiting to the callback', () => {
const fsAsyncWayMethodPath = path.join(os.tmpdir(), 'fs-async-way-method.js')

before(() => {
fs.copyFileSync(path.join(__dirname, 'resources', 'fs-async-way-method.js'), fsAsyncWayMethodPath)
})

after(() => {
fs.unlinkSync(fsAsyncWayMethodPath)
})

testThatRequestHasVulnerability(function () {
const store = storage('legacy').getStore()
const iastCtx = iastContextFunctions.getIastContext(store)
const callArgs = [fsAsyncWayMethodPath]
callArgs[0] = newTaintedString(iastCtx, callArgs[0], 'param', 'Request')
return require(fsAsyncWayMethodPath).doubleCallIgnoringCb('stat', callArgs)
}, 'PATH_TRAVERSAL', { occurrences: 2 })
})
})

describe('test symlink', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@

const fs = require('fs')

module.exports = function (methodName, args, cb) {
function main (methodName, args, cb) {
return new Promise((resolve, reject) => {
fs[methodName](...args, (err, res) => {
resolve(cb(res))
})
})
}

main.doubleCallIgnoringCb = function (methodName, args) {
return new Promise((resolve) => {
fs[methodName](...args, () => {})
fs[methodName](...args, () => {
resolve()
})
})
}

module.exports = main
43 changes: 25 additions & 18 deletions packages/dd-trace/test/appsec/rasp/fs-plugin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('AppsecFsPlugin', () => {
beforeEach(() => {
configure = sinon.stub()
class PluginClass {
addSub (channelName, handler) {}
addBind (channelName, handler) {}

configure (config) {
configure(config)
Expand Down Expand Up @@ -93,20 +93,18 @@ describe('AppsecFsPlugin', () => {
})

describe('_onFsOperationStart', () => {
it('should mark fs root', () => {
it('should return fs root', () => {
const origStore = {}
storage('legacy').enterWith(origStore)

appsecFsPlugin._onFsOperationStart()
let store = appsecFsPlugin._onFsOperationStart()

let store = storage('legacy').getStore()
assert.property(store, 'fs')
assert.propertyVal(store.fs, 'parentStore', origStore)
assert.propertyVal(store.fs, 'root', true)

appsecFsPlugin._onFsOperationFinishOrRenderEnd()
store = appsecFsPlugin._onFsOperationFinishOrRenderEnd()

store = storage('legacy').getStore()
assert.equal(store, origStore)
assert.notProperty(store, 'fs')
})
Expand All @@ -115,28 +113,30 @@ describe('AppsecFsPlugin', () => {
const origStore = { orig: true }
storage('legacy').enterWith(origStore)

appsecFsPlugin._onFsOperationStart()
const rootStore = appsecFsPlugin._onFsOperationStart()

const rootStore = storage('legacy').getStore()
assert.property(rootStore, 'fs')
assert.propertyVal(rootStore.fs, 'parentStore', origStore)
assert.propertyVal(rootStore.fs, 'root', true)

appsecFsPlugin._onFsOperationStart()
storage('legacy').enterWith(rootStore)

let store = appsecFsPlugin._onFsOperationStart()

let store = storage('legacy').getStore()
assert.property(store, 'fs')
assert.propertyVal(store.fs, 'parentStore', rootStore)
assert.propertyVal(store.fs, 'root', false)
assert.propertyVal(store, 'orig', true)

appsecFsPlugin._onFsOperationFinishOrRenderEnd()
storage('legacy').enterWith(store)

store = appsecFsPlugin._onFsOperationFinishOrRenderEnd()

store = storage('legacy').getStore()
assert.equal(store, rootStore)

appsecFsPlugin._onFsOperationFinishOrRenderEnd()
store = storage('legacy').getStore()
storage('legacy').enterWith(store)

store = appsecFsPlugin._onFsOperationFinishOrRenderEnd()
assert.equal(store, origStore)
})
})
Expand All @@ -148,16 +148,16 @@ describe('AppsecFsPlugin', () => {
const origStore = {}
storage('legacy').enterWith(origStore)

appsecFsPlugin._onResponseRenderStart()
let store = appsecFsPlugin._onResponseRenderStart()

let store = storage('legacy').getStore()
assert.property(store, 'fs')
assert.propertyVal(store.fs, 'parentStore', origStore)
assert.propertyVal(store.fs, 'opExcluded', true)

appsecFsPlugin._onFsOperationFinishOrRenderEnd()
storage('legacy').enterWith(store)

store = appsecFsPlugin._onFsOperationFinishOrRenderEnd()

store = storage('legacy').getStore()
assert.equal(store, origStore)
assert.notProperty(store, 'fs')
})
Expand Down Expand Up @@ -225,6 +225,12 @@ describe('AppsecFsPlugin', () => {

it('should clean up store when finishing op', () => {
let count = 4
// TODO Remove this when node 18 is unsupported or dc-polyfill is fixed&updated
// hack to node 18 and early 20.x
// with dc-polyfill addBind is not enough to force a channel.hasSubscribers === true
const onStart = () => {}
opStartCh.subscribe(onStart)

const onFinish = () => {
const store = storage('legacy').getStore()
count--
Expand All @@ -244,6 +250,7 @@ describe('AppsecFsPlugin', () => {
assert.strictEqual(count, 0)
} finally {
opFinishCh.unsubscribe(onFinish)
opStartCh.unsubscribe(onStart)
}
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,13 @@ describe('RASP - lfi', () => {

describe('test readFile', () => {
runFsMethodTestThreeWay('readFile', undefined, __filename)

runFsMethodTest('an async operation without callback is executed before',
{ getAppFn: getAppSync, ruleEvalCount: 2 }, (args) => {
const fs = require('fs')
fs.readFile(path.join(__dirname, 'utils.js'), () => {}) // safe and ignored operation
return fs.readFileSync(...args)
}, __filename)
})

describe('test readlink', () => {
Expand Down
Loading