Skip to content

Commit

Permalink
multer instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
iunanua committed Oct 15, 2024
1 parent 944f57d commit 6e6dadc
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 13 deletions.
1 change: 1 addition & 0 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ module.exports = {
'mongodb-core': () => require('../mongodb-core'),
mongoose: () => require('../mongoose'),
mquery: () => require('../mquery'),
multer: () => require('../multer'),
mysql: () => require('../mysql'),
mysql2: () => require('../mysql2'),
net: () => require('../net'),
Expand Down
37 changes: 37 additions & 0 deletions packages/datadog-instrumentations/src/multer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict'

const shimmer = require('../../datadog-shimmer')
const { channel, addHook, AsyncResource } = require('./helpers/instrument')

const multerReadCh = channel('datadog:multer:read:finish')

function publishRequestBodyAndNext (req, res, next) {
return shimmer.wrapFunction(next, next => function () {
if (multerReadCh.hasSubscribers && req) {
const abortController = new AbortController()
const body = req.body

multerReadCh.publish({ req, res, body, abortController })

if (abortController.signal.aborted) return
}

return next.apply(this, arguments)
})
}

addHook({
name: 'multer',
file: 'lib/make-middleware.js',
versions: ['>=1.4.4 < 2.0.0']
}, makeMiddleware => {
return shimmer.wrapFunction(makeMiddleware, makeMiddleware => function () {
const middleware = makeMiddleware.apply(this, arguments)

return shimmer.wrapFunction(middleware, middleware => function wrapMulterMiddleware (req, res, next) {
const nextResource = new AsyncResource('bound-anonymous-fn')
arguments[2] = nextResource.bind(publishRequestBodyAndNext(req, res, next))
return middleware.apply(this, arguments)
})
})
})
105 changes: 105 additions & 0 deletions packages/datadog-instrumentations/test/multer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use strict'

const dc = require('dc-polyfill')
const axios = require('axios')
const agent = require('../../dd-trace/test/plugins/agent')
const { storage } = require('../../datadog-core')

withVersions('multer', 'multer', version => {
describe('multer parser instrumentation', () => {
const multerReadCh = dc.channel('datadog:multer:read:finish')
let port, server, middlewareProcessBodyStub, formData

before(() => {
return agent.load(['http', 'express', 'multer'], { client: false })
})

before((done) => {
const express = require('../../../versions/express').get()
const multer = require(`../../../versions/multer@${version}`).get()
const uploadToMemory = multer({ storage: multer.memoryStorage(), limits: { fileSize: 200000 } })

const app = express()

app.post('/', uploadToMemory.single('file'), (req, res) => {
middlewareProcessBodyStub(req.body.key)
res.end('DONE')
})
server = app.listen(0, () => {
port = server.address().port
done()
})
})

beforeEach(async () => {
middlewareProcessBodyStub = sinon.stub()

formData = new FormData()
formData.append('key', 'value')
})

after(() => {
server.close()
return agent.close({ ritmReset: false })
})

it('should not abort the request by default', async () => {
const res = await axios.post(`http://localhost:${port}/`, formData)

expect(middlewareProcessBodyStub).to.be.calledOnceWithExactly(formData.get('key'))
expect(res.data).to.be.equal('DONE')
})

it('should not abort the request with non blocker subscription', async () => {
function noop () {}
multerReadCh.subscribe(noop)

const form = new FormData()
form.append('key', 'value')

const res = await axios.post(`http://localhost:${port}/`, formData)

expect(middlewareProcessBodyStub).to.be.calledOnceWithExactly(formData.get('key'))
expect(res.data).to.be.equal('DONE')

multerReadCh.unsubscribe(noop)
})

it('should abort the request when abortController.abort() is called', async () => {
function blockRequest ({ res, abortController }) {
res.end('BLOCKED')
abortController.abort()
}
multerReadCh.subscribe(blockRequest)

const res = await axios.post(`http://localhost:${port}/`, formData)

expect(middlewareProcessBodyStub).not.to.be.called
expect(res.data).to.be.equal('BLOCKED')

multerReadCh.unsubscribe(blockRequest)
})

it('should not lose the http async context', async () => {
let store
let payload

function handler (data) {
store = storage.getStore()
payload = data
}
multerReadCh.subscribe(handler)

const res = await axios.post(`http://localhost:${port}/`, formData)

expect(store).to.have.property('req', payload.req)
expect(store).to.have.property('res', payload.res)
expect(store).to.have.property('span')

expect(middlewareProcessBodyStub).to.be.calledOnceWithExactly(formData.get('key'))
expect(res.data).to.be.equal('DONE')

multerReadCh.unsubscribe(handler)
})
})
})
1 change: 1 addition & 0 deletions packages/dd-trace/src/appsec/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const dc = require('dc-polyfill')
module.exports = {
bodyParser: dc.channel('datadog:body-parser:read:finish'),
cookieParser: dc.channel('datadog:cookie-parser:read:finish'),
multerParser: dc.channel('datadog:multer:read:finish'),
startGraphqlResolve: dc.channel('datadog:graphql:resolver:start'),
graphqlMiddlewareChannel: dc.tracingChannel('datadog:apollo:middleware'),
apolloChannel: dc.tracingChannel('datadog:apollo:request'),
Expand Down
21 changes: 14 additions & 7 deletions packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,22 @@ class TaintTrackingPlugin extends SourceIastPlugin {
}

onConfigure () {
const onRequestBody = ({ req }) => {
const iastContext = getIastContext(storage.getStore())
if (iastContext && iastContext.body !== req.body) {
this._taintTrackingHandler(HTTP_REQUEST_BODY, req, 'body', iastContext)
iastContext.body = req.body
}
}

this.addSub(
{ channelName: 'datadog:body-parser:read:finish', tag: HTTP_REQUEST_BODY },
({ req }) => {
const iastContext = getIastContext(storage.getStore())
if (iastContext && iastContext.body !== req.body) {
this._taintTrackingHandler(HTTP_REQUEST_BODY, req, 'body', iastContext)
iastContext.body = req.body
}
}
onRequestBody
)

this.addSub(
{ channelName: 'datadog:multer:read:finish', tag: HTTP_REQUEST_BODY },
onRequestBody
)

this.addSub(
Expand Down
2 changes: 2 additions & 0 deletions packages/dd-trace/src/appsec/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const remoteConfig = require('./remote_config')
const {
bodyParser,
cookieParser,
multerParser,
incomingHttpRequestStart,
incomingHttpRequestEnd,
passportVerify,
Expand Down Expand Up @@ -57,6 +58,7 @@ function enable (_config) {
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
bodyParser.subscribe(onRequestBodyParsed)
multerParser.subscribe(onRequestBodyParsed)
nextBodyParsed.subscribe(onRequestBodyParsed)
nextQueryParsed.subscribe(onRequestQueryParsed)
queryParser.subscribe(onRequestQueryParsed)
Expand Down
13 changes: 7 additions & 6 deletions packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ describe('IAST Taint tracking plugin', () => {
})

it('Should subscribe to body parser, qs, cookie and process_params channel', () => {
expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(6)
expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(7)
expect(taintTrackingPlugin._subscriptions[0]._channel.name).to.equals('datadog:body-parser:read:finish')
expect(taintTrackingPlugin._subscriptions[1]._channel.name).to.equals('datadog:qs:parse:finish')
expect(taintTrackingPlugin._subscriptions[2]._channel.name).to.equals('apm:express:middleware:next')
expect(taintTrackingPlugin._subscriptions[3]._channel.name).to.equals('datadog:cookie:parse:finish')
expect(taintTrackingPlugin._subscriptions[4]._channel.name).to.equals('datadog:express:process_params:start')
expect(taintTrackingPlugin._subscriptions[5]._channel.name).to.equals('apm:graphql:resolve:start')
expect(taintTrackingPlugin._subscriptions[1]._channel.name).to.equals('datadog:multer:read:finish')
expect(taintTrackingPlugin._subscriptions[2]._channel.name).to.equals('datadog:qs:parse:finish')
expect(taintTrackingPlugin._subscriptions[3]._channel.name).to.equals('apm:express:middleware:next')
expect(taintTrackingPlugin._subscriptions[4]._channel.name).to.equals('datadog:cookie:parse:finish')
expect(taintTrackingPlugin._subscriptions[5]._channel.name).to.equals('datadog:express:process_params:start')
expect(taintTrackingPlugin._subscriptions[6]._channel.name).to.equals('apm:graphql:resolve:start')
})

describe('taint sources', () => {
Expand Down
82 changes: 82 additions & 0 deletions packages/dd-trace/test/appsec/index.multer.plugin.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict'

const { channel } = require('dc-polyfill')
const axios = require('axios')
const path = require('path')
const agent = require('../plugins/agent')
const appsec = require('../../src/appsec')
const Config = require('../../src/config')
const { json } = require('../../src/appsec/blocked_templates')

const multerReadCh = channel('datadog:multer:read:finish')

withVersions('multer', 'multer', version => {
describe('Suspicious request blocking - multer', () => {
let port, server, requestBody, onMulterRead

before(() => {
return agent.load(['express', 'multer', 'http'], { client: false })
})

before((done) => {
const express = require('../../../../versions/express').get()
const multer = require(`../../../../versions/multer@${version}`).get()
const uploadToMemory = multer({ storage: multer.memoryStorage(), limits: { fileSize: 200000 } })

const app = express()

app.post('/', uploadToMemory.single('file'), (req, res) => {
requestBody(req)
res.end('DONE')
})

server = app.listen(port, () => {
port = server.address().port
done()
})
})

beforeEach(async () => {
requestBody = sinon.stub()
onMulterRead = sinon.stub()
multerReadCh.subscribe(onMulterRead)

appsec.enable(new Config({ appsec: { enabled: true, rules: path.join(__dirname, 'body-parser-rules.json') } }))
})

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

after(() => {
server.close()
return agent.close({ ritmReset: false })
})

it('should not block the request without an attack', async () => {
const form = new FormData()
form.append('key', 'value')

const res = await axios.post(`http://localhost:${port}/`, form)

expect(requestBody).to.be.calledOnce
expect(res.data).to.be.equal('DONE')
})

it('should block the request when attack is detected', async () => {
try {
const form = new FormData()
form.append('key', 'testattack')

await axios.post(`http://localhost:${port}/`, form)

return Promise.reject(new Error('Request should not return 200'))
} catch (e) {
expect(e.response.status).to.be.equals(403)
expect(e.response.data).to.be.deep.equal(JSON.parse(json))
expect(requestBody).not.to.be.called
}
})
})
})

0 comments on commit 6e6dadc

Please sign in to comment.