Skip to content

Allow blocking on fastify cookie #5910

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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
33 changes: 29 additions & 4 deletions packages/datadog-instrumentations/src/fastify.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ const handleChannel = channel('apm:fastify:request:handle')
const routeAddedChannel = channel('apm:fastify:route:added')
const bodyParserReadCh = channel('datadog:fastify:body-parser:finish')
const queryParamsReadCh = channel('datadog:fastify:query-params:finish')
const cookieParserReadCh = channel('datadog:fastify-cookie:read:finish')
const responsePayloadReadCh = channel('datadog:fastify:response:finish')
const pathParamsReadCh = channel('datadog:fastify:path-params:finish')

const parsingResources = new WeakMap()
const cookiesPublished = new WeakSet()

function wrapFastify (fastify, hasParsingEvents) {
if (typeof fastify !== 'function') return fastify
Expand Down Expand Up @@ -49,26 +51,46 @@ function wrapAddHook (addHook) {
const req = getReq(request)

try {
if (typeof done === 'function') {
done = arguments[arguments.length - 1]
// done callback is always the last argument
const doneCallback = arguments[arguments.length - 1]

if (typeof doneCallback === 'function') {
arguments[arguments.length - 1] = function (err) {
publishError(err, req)

const hasCookies = request.cookies && Object.keys(request.cookies).length > 0

if (cookieParserReadCh.hasSubscribers && hasCookies && !cookiesPublished.has(req)) {
const res = getRes(reply)
const abortController = new AbortController()

cookieParserReadCh.publish({
req,
res,
abortController,
cookies: request.cookies
})

cookiesPublished.add(req)

if (abortController.signal.aborted) return
}

if (name === 'onRequest' || name === 'preParsing') {
const parsingResource = new AsyncResource('bound-anonymous-fn')

parsingResources.set(req, parsingResource)

return parsingResource.runInAsyncScope(() => {
return done.apply(this, arguments)
return doneCallback.apply(this, arguments)
})
}
return done.apply(this, arguments)
return doneCallback.apply(this, arguments)
}

return fn.apply(this, arguments)
}

const promise = fn.apply(this, arguments)

if (promise && typeof promise.catch === 'function') {
Expand Down Expand Up @@ -118,6 +140,7 @@ function preValidation (request, reply, done) {

if (queryParamsReadCh.hasSubscribers && request.query) {
abortController ??= new AbortController()

queryParamsReadCh.publish({
req,
res,
Expand All @@ -130,13 +153,15 @@ function preValidation (request, reply, done) {

if (bodyParserReadCh.hasSubscribers && request.body) {
abortController ??= new AbortController()

bodyParserReadCh.publish({ req, res, body: request.body, abortController })

if (abortController.signal.aborted) return
}

if (pathParamsReadCh.hasSubscribers && request.params) {
abortController ??= new AbortController()

pathParamsReadCh.publish({
req,
res,
Expand Down
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 @@ -15,6 +15,7 @@ module.exports = {
fastifyBodyParser: dc.channel('datadog:fastify:body-parser:finish'),
fastifyResponseChannel: dc.channel('datadog:fastify:response:finish'),
fastifyQueryParams: dc.channel('datadog:fastify:query-params:finish'),
fastifyCookieParser: dc.channel('datadog:fastify-cookie:read:finish'),
fastifyPathParams: dc.channel('datadog:fastify:path-params:finish'),
fsOperationStart: dc.channel('apm:fs:operation:start'),
graphqlMiddlewareChannel: dc.tracingChannel('datadog:apollo:middleware'),
Expand Down
3 changes: 3 additions & 0 deletions packages/dd-trace/src/appsec/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
cookieParser,
multerParser,
fastifyBodyParser,
fastifyCookieParser,
incomingHttpRequestStart,
incomingHttpRequestEnd,
passportVerify,
Expand Down Expand Up @@ -83,6 +84,7 @@ function enable (_config) {
expressProcessParams.subscribe(onRequestProcessParams)
fastifyBodyParser.subscribe(onRequestBodyParsed)
fastifyQueryParams.subscribe(onRequestQueryParsed)
fastifyCookieParser.subscribe(onRequestCookieParser)
fastifyPathParams.subscribe(onRequestProcessParams)
routerParam.subscribe(onRequestProcessParams)
responseBody.subscribe(onResponseBody)
Expand Down Expand Up @@ -377,6 +379,7 @@ function disable () {
if (expressProcessParams.hasSubscribers) expressProcessParams.unsubscribe(onRequestProcessParams)
if (fastifyBodyParser.hasSubscribers) fastifyBodyParser.unsubscribe(onRequestBodyParsed)
if (fastifyQueryParams.hasSubscribers) fastifyQueryParams.unsubscribe(onRequestQueryParsed)
if (fastifyCookieParser.hasSubscribers) fastifyCookieParser.unsubscribe(onRequestCookieParser)
if (fastifyPathParams.hasSubscribers) fastifyPathParams.unsubscribe(onRequestProcessParams)
if (routerParam.hasSubscribers) routerParam.unsubscribe(onRequestProcessParams)
if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
Expand Down
123 changes: 106 additions & 17 deletions packages/dd-trace/test/appsec/index.fastify.plugin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ const Axios = require('axios')
const { assert } = require('chai')
const getPort = require('get-port')
const path = require('path')
const semver = require('semver')
const zlib = require('zlib')
const fs = require('node:fs')
const agent = require('../plugins/agent')
const appsec = require('../../src/appsec')
const Config = require('../../src/config')
const { json } = require('../../src/appsec/blocked_templates')

withVersions('fastify', 'fastify', version => {
withVersions('fastify', 'fastify', '>=2', version => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't you removing coverage for fastify v1 by doing this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not support fastify v1, I will add this to the documentation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not ? the instrumentation is there no ?

describe('Suspicious request blocking - query', () => {
let server, requestBody, axios

Expand Down Expand Up @@ -234,11 +233,6 @@ withVersions('fastify', 'fastify', version => {
})

it('should return 403 for dangerous payloads', async () => {
// Skip Fastify v1 - different behavior where schema validation takes precedence
if (semver.lt(semver.coerce(version), '2.0.0')) {
return
}

try {
await axios.post('/schema-validated', { key: 'testattack' })

Expand All @@ -250,11 +244,6 @@ withVersions('fastify', 'fastify', version => {
})

it('should return 403 for valid schema with attack content', async () => {
// Skip Fastify v1 - different behavior where schema validation takes precedence
if (semver.lt(semver.coerce(version), '2.0.0')) {
return
}

try {
await axios.post('/schema-validated', { validField: 'testattack' })

Expand All @@ -267,11 +256,6 @@ withVersions('fastify', 'fastify', version => {
})

describe('Suspicious request blocking - path parameters', () => {
// Skip Fastify v1 - preValidation hook is not supported
if (semver.lt(semver.coerce(version), '2.0.0')) {
return
}

let server, preHandlerHookSpy, preValidationHookSpy, axios

before(() => {
Expand Down Expand Up @@ -447,6 +431,111 @@ withVersions('fastify', 'fastify', version => {
})
})
})

describe('Suspicious request blocking - cookie', () => {
withVersions('fastify', '@fastify/cookie', cookieVersion => {
const hookConfigurations = [
'onRequest',
'preParsing',
'preValidation',
'preHandler'
]

hookConfigurations.forEach((hook) => {
describe(`with ${hook} hook`, () => {
let server, requestCookie, axios

before(function () {
if (version === '3.9.2') {
// Fastify 3.9.2 is incompatible with @fastify/cookie >=6
this.skip()
}

// Skip preParsing hook for Fastify 2.x - has compatibility issues
if (hook === 'preParsing' && version.startsWith('2')) {
this.skip()
}

return agent.load(['fastify', '@fastify/cookie', 'http'], { client: false })
})

before((done) => {
const fastify = require(`../../../../versions/fastify@${version}`).get()
const fastifyCookie = require(`../../../../versions/@fastify/cookie@${cookieVersion}`).get()

const app = fastify()

app.register(fastifyCookie, {
secret: 'my-secret',
hook
})

// Dummy hook
app.addHook('onRequest', (req, reply, done) => done())

app.post('/', (request, reply) => {
requestCookie()
reply.send('DONE')
})

getPort().then((port) => {
app.listen({ port }, () => {
axios = Axios.create({ baseURL: `http://localhost:${port}` })
done()
})
server = app.server
}).catch(done)
})

beforeEach(async () => {
requestCookie = sinon.stub()
appsec.enable(
new Config({
appsec: {
enabled: true,
rules: path.join(__dirname, 'cookie-parser-rules.json')
}
})
)
})

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

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

it('should not block the request without an attack', async () => {
const res = await axios.post('/', {})

sinon.assert.calledOnce(requestCookie)
assert.strictEqual(res.data, 'DONE')
})

it('should block the request when attack is detected', async () => {
try {
await axios.post('/', {}, {
headers: {
Cookie: 'key=testattack'
}
})

return Promise.reject(new Error('Request should not return 200'))
} catch (e) {
assert.strictEqual(e.response.status, 403)
assert.deepEqual(e.response.data, JSON.parse(json))
sinon.assert.notCalled(requestCookie)
}
})
})
})
})
})
})

describe('Api Security - Fastify', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/dd-trace/test/plugins/externals.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@
{
"name": "middie",
"versions": ["5.1.0"]
},
{
"name": "@fastify/cookie",
"versions": [">=6"]
}
],
"generic-pool": [
Expand Down