Skip to content

Commit 020c925

Browse files
authored
Allow blocking on fastify path params (#5889)
Allow blocking on fastify path params
1 parent f88b9af commit 020c925

File tree

4 files changed

+206
-7
lines changed

4 files changed

+206
-7
lines changed

packages/datadog-instrumentations/src/fastify.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const handleChannel = channel('apm:fastify:request:handle')
88
const routeAddedChannel = channel('apm:fastify:route:added')
99
const bodyParserReadCh = channel('datadog:fastify:body-parser:finish')
1010
const queryParamsReadCh = channel('datadog:fastify:query-params:finish')
11+
const pathParamsReadCh = channel('datadog:fastify:path-params:finish')
1112

1213
const parsingResources = new WeakMap()
1314

@@ -112,9 +113,10 @@ function preValidation (request, reply, done) {
112113
const parsingResource = parsingResources.get(req)
113114

114115
const processInContext = () => {
115-
if (queryParamsReadCh.hasSubscribers && request.query) {
116-
const abortController = new AbortController()
116+
let abortController
117117

118+
if (queryParamsReadCh.hasSubscribers && request.query) {
119+
abortController ??= new AbortController()
118120
queryParamsReadCh.publish({
119121
req,
120122
res,
@@ -126,13 +128,24 @@ function preValidation (request, reply, done) {
126128
}
127129

128130
if (bodyParserReadCh.hasSubscribers && request.body) {
129-
const abortController = new AbortController()
130-
131+
abortController ??= new AbortController()
131132
bodyParserReadCh.publish({ req, res, body: request.body, abortController })
132133

133134
if (abortController.signal.aborted) return
134135
}
135136

137+
if (pathParamsReadCh.hasSubscribers && request.params) {
138+
abortController ??= new AbortController()
139+
pathParamsReadCh.publish({
140+
req,
141+
res,
142+
abortController,
143+
params: request.params
144+
})
145+
146+
if (abortController.signal.aborted) return
147+
}
148+
136149
done()
137150
}
138151

packages/dd-trace/src/appsec/channels.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module.exports = {
1414
expressSession: dc.channel('datadog:express-session:middleware:finish'),
1515
fastifyBodyParser: dc.channel('datadog:fastify:body-parser:finish'),
1616
fastifyQueryParams: dc.channel('datadog:fastify:query-params:finish'),
17+
fastifyPathParams: dc.channel('datadog:fastify:path-params:finish'),
1718
fsOperationStart: dc.channel('apm:fs:operation:start'),
1819
graphqlMiddlewareChannel: dc.tracingChannel('datadog:apollo:middleware'),
1920
httpClientRequestStart: dc.channel('apm:http:client:request:start'),

packages/dd-trace/src/appsec/index.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const {
2121
responseBody,
2222
responseWriteHead,
2323
responseSetHeader,
24-
routerParam
24+
routerParam,
25+
fastifyPathParams
2526
} = require('./channels')
2627
const waf = require('./waf')
2728
const addresses = require('./addresses')
@@ -69,7 +70,6 @@ function enable (_config) {
6970

7071
bodyParser.subscribe(onRequestBodyParsed)
7172
multerParser.subscribe(onRequestBodyParsed)
72-
fastifyBodyParser.subscribe(onRequestBodyParsed)
7373
cookieParser.subscribe(onRequestCookieParser)
7474
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
7575
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
@@ -80,7 +80,9 @@ function enable (_config) {
8080
nextBodyParsed.subscribe(onRequestBodyParsed)
8181
nextQueryParsed.subscribe(onRequestQueryParsed)
8282
expressProcessParams.subscribe(onRequestProcessParams)
83+
fastifyBodyParser.subscribe(onRequestBodyParsed)
8384
fastifyQueryParams.subscribe(onRequestQueryParsed)
85+
fastifyPathParams.subscribe(onRequestProcessParams)
8486
routerParam.subscribe(onRequestProcessParams)
8587
responseBody.subscribe(onResponseBody)
8688
responseWriteHead.subscribe(onResponseWriteHead)
@@ -361,7 +363,6 @@ function disable () {
361363
// Channel#unsubscribe() is undefined for non active channels
362364
if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
363365
if (multerParser.hasSubscribers) multerParser.unsubscribe(onRequestBodyParsed)
364-
if (fastifyBodyParser.hasSubscribers) fastifyBodyParser.unsubscribe(onRequestBodyParsed)
365366
if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
366367
if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
367368
if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator)
@@ -372,7 +373,9 @@ function disable () {
372373
if (nextBodyParsed.hasSubscribers) nextBodyParsed.unsubscribe(onRequestBodyParsed)
373374
if (nextQueryParsed.hasSubscribers) nextQueryParsed.unsubscribe(onRequestQueryParsed)
374375
if (expressProcessParams.hasSubscribers) expressProcessParams.unsubscribe(onRequestProcessParams)
376+
if (fastifyBodyParser.hasSubscribers) fastifyBodyParser.unsubscribe(onRequestBodyParsed)
375377
if (fastifyQueryParams.hasSubscribers) fastifyQueryParams.unsubscribe(onRequestQueryParsed)
378+
if (fastifyPathParams.hasSubscribers) fastifyPathParams.unsubscribe(onRequestProcessParams)
376379
if (routerParam.hasSubscribers) routerParam.unsubscribe(onRequestProcessParams)
377380
if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
378381
if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)

packages/dd-trace/test/appsec/index.fastify.plugin.spec.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,188 @@ withVersions('fastify', 'fastify', version => {
262262
}
263263
})
264264
})
265+
266+
describe('Suspicious request blocking - path parameters', () => {
267+
// Skip Fastify v1 - preValidation hook is not supported
268+
if (semver.lt(semver.coerce(version), '2.0.0')) {
269+
return
270+
}
271+
272+
let server, preHandlerHookSpy, preValidationHookSpy, axios
273+
274+
before(() => {
275+
return agent.load(['fastify', 'http'], { client: false })
276+
})
277+
278+
before((done) => {
279+
const fastify = require(`../../../../versions/fastify@${version}`).get()
280+
281+
const app = fastify()
282+
app.get('/multiple-path-params/:parameter1/:parameter2', (request, reply) => {
283+
reply.send('DONE')
284+
})
285+
286+
app.register(async function (nested) {
287+
nested.get('/:nestedParam', async (request, reply) => {
288+
reply.send('DONE')
289+
})
290+
}, { prefix: '/nested/:parentParam' })
291+
292+
const paramHook = (request, reply, done) => {
293+
done()
294+
}
295+
296+
preHandlerHookSpy = sinon.spy(paramHook)
297+
298+
app.addHook('preHandler', preHandlerHookSpy)
299+
300+
const validationHook = (request, reply, done) => {
301+
done()
302+
}
303+
304+
preValidationHookSpy = sinon.spy(validationHook)
305+
app.addHook('preValidation', preValidationHookSpy)
306+
307+
app.get('/callback-path-param/:pathParameter', (request, reply) => {
308+
reply.send('DONE')
309+
})
310+
311+
getPort().then((port) => {
312+
app.listen({ port }, () => {
313+
axios = Axios.create({ baseURL: `http://localhost:${port}` })
314+
done()
315+
})
316+
server = app.server
317+
})
318+
})
319+
320+
after(() => {
321+
server.close()
322+
return agent.close({ ritmReset: false })
323+
})
324+
325+
beforeEach(async () => {
326+
appsec.enable(new Config({
327+
appsec: {
328+
enabled: true,
329+
rules: path.join(__dirname, 'rules-example.json')
330+
}
331+
}))
332+
})
333+
334+
afterEach(() => {
335+
appsec.disable()
336+
sinon.reset()
337+
})
338+
339+
describe('route with multiple path parameters', () => {
340+
it('should not block the request when attack is not detected', async () => {
341+
const res = await axios.get('/multiple-path-params/safe_param/safe_param')
342+
343+
assert.equal(res.status, 200)
344+
assert.equal(res.data, 'DONE')
345+
})
346+
347+
it('should block the request when attack is detected in both parameters', async () => {
348+
try {
349+
await axios.get('/multiple-path-params/testattack/testattack')
350+
351+
return Promise.reject(new Error('Request should not return 200'))
352+
} catch (e) {
353+
assert.equal(e.response.status, 403)
354+
assert.deepEqual(e.response.data, JSON.parse(json))
355+
}
356+
})
357+
358+
it('should block the request when attack is detected in the first parameter', async () => {
359+
try {
360+
await axios.get('/multiple-path-params/testattack/safe_param')
361+
362+
return Promise.reject(new Error('Request should not return 200'))
363+
} catch (e) {
364+
assert.equal(e.response.status, 403)
365+
assert.deepEqual(e.response.data, JSON.parse(json))
366+
}
367+
})
368+
369+
it('should block the request when attack is detected in the second parameter', async () => {
370+
try {
371+
await axios.get('/multiple-path-params/safe_param/testattack')
372+
373+
return Promise.reject(new Error('Request should not return 200'))
374+
} catch (e) {
375+
assert.equal(e.response.status, 403)
376+
assert.deepEqual(e.response.data, JSON.parse(json))
377+
}
378+
})
379+
})
380+
381+
describe('nested routes', () => {
382+
it('should not block the request when attack is not detected', async () => {
383+
const res = await axios.get('/nested/safe_param/safe_param')
384+
385+
assert.equal(res.status, 200)
386+
assert.equal(res.data, 'DONE')
387+
})
388+
389+
it('should block the request when attack is detected in the nested parameter', async () => {
390+
try {
391+
await axios.get('/nested/safe_param/testattack')
392+
393+
return Promise.reject(new Error('Request should not return 200'))
394+
} catch (e) {
395+
assert.equal(e.response.status, 403)
396+
assert.deepEqual(e.response.data, JSON.parse(json))
397+
}
398+
})
399+
400+
it('should block the request when attack is detected in the parent parameter', async () => {
401+
try {
402+
await axios.get('/nested/testattack/safe_param')
403+
404+
return Promise.reject(new Error('Request should not return 200'))
405+
} catch (e) {
406+
assert.equal(e.response.status, 403)
407+
assert.deepEqual(e.response.data, JSON.parse(json))
408+
}
409+
})
410+
411+
it('should block the request when attack is detected in both parameters', async () => {
412+
try {
413+
await axios.get('/nested/testattack/testattack')
414+
415+
return Promise.reject(new Error('Request should not return 200'))
416+
} catch (e) {
417+
assert.equal(e.response.status, 403)
418+
assert.deepEqual(e.response.data, JSON.parse(json))
419+
}
420+
})
421+
})
422+
423+
describe('path parameter with hook', () => {
424+
it('should not block the request when attack is not detected', async () => {
425+
const res = await axios.get('/callback-path-param/safe_param')
426+
427+
assert.equal(res.status, 200)
428+
assert.equal(res.data, 'DONE')
429+
sinon.assert.calledOnce(preHandlerHookSpy)
430+
sinon.assert.calledOnce(preValidationHookSpy)
431+
})
432+
433+
it('should block the request when attack is detected', async () => {
434+
try {
435+
await axios.get('/callback-path-param/testattack')
436+
437+
return Promise.reject(new Error('Request should not return 200'))
438+
} catch (e) {
439+
assert.equal(e.response.status, 403)
440+
assert.deepEqual(e.response.data, JSON.parse(json))
441+
sinon.assert.notCalled(preHandlerHookSpy)
442+
sinon.assert.notCalled(preValidationHookSpy)
443+
}
444+
})
445+
})
446+
})
265447
})
266448

267449
const createNestedObject = (n, obj) => {

0 commit comments

Comments
 (0)