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

Suspicious request blocking - Express Path Parameters #4769

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
18 changes: 14 additions & 4 deletions packages/datadog-instrumentations/src/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,21 @@ addHook({
})

const processParamsStartCh = channel('datadog:express:process_params:start')
const wrapProcessParamsMethod = (requestPositionInArguments) => {
return (original) => {
return function () {
function wrapProcessParamsMethod (requestPositionInArguments) {
return function wrapProcessParams (original) {
return function wrappedProcessParams () {
if (processParamsStartCh.hasSubscribers) {
processParamsStartCh.publish({ req: arguments[requestPositionInArguments] })
const req = arguments[requestPositionInArguments]
const abortController = new AbortController()

processParamsStartCh.publish({
req,
res: req?.res,
abortController,
params: req?.params
})

if (abortController.signal.aborted) return
}

return original.apply(this, arguments)
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 @@ -17,6 +17,7 @@ module.exports = {
setCookieChannel: dc.channel('datadog:iast:set-cookie'),
nextBodyParsed: dc.channel('apm:next:body-parsed'),
nextQueryParsed: dc.channel('apm:next:query-parsed'),
expressProcessParams: dc.channel('datadog:express:process_params:start'),
responseBody: dc.channel('datadog:express:response:json:start'),
responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
httpClientRequestStart: dc.channel('apm:http:client:request:start'),
Expand Down
99 changes: 56 additions & 43 deletions packages/dd-trace/src/appsec/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
queryParser,
nextBodyParsed,
nextQueryParsed,
expressProcessParams,
responseBody,
responseWriteHead,
responseSetHeader
Expand All @@ -30,6 +31,8 @@ const { storage } = require('../../../datadog-core')
const graphql = require('./graphql')
const rasp = require('./rasp')

const responseAnalyzedSet = new WeakSet()

let isEnabled = false
let config

Expand All @@ -54,13 +57,14 @@ function enable (_config) {

apiSecuritySampler.configure(_config.appsec)

bodyParser.subscribe(onRequestBodyParsed)
cookieParser.subscribe(onRequestCookieParser)
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
bodyParser.subscribe(onRequestBodyParsed)
queryParser.subscribe(onRequestQueryParsed)
nextBodyParsed.subscribe(onRequestBodyParsed)
nextQueryParsed.subscribe(onRequestQueryParsed)
queryParser.subscribe(onRequestQueryParsed)
cookieParser.subscribe(onRequestCookieParser)
expressProcessParams.subscribe(onRequestProcessParams)
responseBody.subscribe(onResponseBody)
responseWriteHead.subscribe(onResponseWriteHead)
responseSetHeader.subscribe(onResponseSetHeader)
Expand All @@ -79,6 +83,41 @@ function enable (_config) {
}
}

function onRequestBodyParsed ({ req, res, body, abortController }) {
if (body === undefined || body === null) return

if (!req) {
const store = storage.getStore()
req = store?.req
}

const rootSpan = web.root(req)
if (!rootSpan) return

const results = waf.run({
persistent: {
[addresses.HTTP_INCOMING_BODY]: body
}
}, req)

handleResults(results, req, res, rootSpan, abortController)
}

function onRequestCookieParser ({ req, res, abortController, cookies }) {
if (!cookies || typeof cookies !== 'object') return

const rootSpan = web.root(req)
if (!rootSpan) return

const results = waf.run({
persistent: {
[addresses.HTTP_INCOMING_COOKIES]: cookies
}
}, req)

handleResults(results, req, res, rootSpan, abortController)
}

function incomingHttpStartTranslator ({ req, res, abortController }) {
const rootSpan = web.root(req)
if (!rootSpan) return
Expand Down Expand Up @@ -122,11 +161,6 @@ function incomingHttpEndTranslator ({ req, res }) {
persistent[addresses.HTTP_INCOMING_BODY] = req.body
}

// TODO: temporary express instrumentation, will use express plugin later
if (req.params !== null && typeof req.params === 'object') {
persistent[addresses.HTTP_INCOMING_PARAMS] = req.params
}

// we need to keep this to support other cookie parsers
if (req.cookies !== null && typeof req.cookies === 'object') {
persistent[addresses.HTTP_INCOMING_COOKIES] = req.cookies
Expand All @@ -145,24 +179,16 @@ function incomingHttpEndTranslator ({ req, res }) {
Reporter.finishRequest(req, res)
}

function onRequestBodyParsed ({ req, res, body, abortController }) {
if (body === undefined || body === null) return
function onPassportVerify ({ credentials, user }) {
const store = storage.getStore()
const rootSpan = store?.req && web.root(store.req)

if (!req) {
const store = storage.getStore()
req = store?.req
if (!rootSpan) {
log.warn('No rootSpan found in onPassportVerify')
return
}

const rootSpan = web.root(req)
if (!rootSpan) return

const results = waf.run({
persistent: {
[addresses.HTTP_INCOMING_BODY]: body
}
}, req)

handleResults(results, req, res, rootSpan, abortController)
passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
}

function onRequestQueryParsed ({ req, res, query, abortController }) {
Expand All @@ -185,15 +211,15 @@ function onRequestQueryParsed ({ req, res, query, abortController }) {
handleResults(results, req, res, rootSpan, abortController)
}

function onRequestCookieParser ({ req, res, abortController, cookies }) {
if (!cookies || typeof cookies !== 'object') return

function onRequestProcessParams ({ req, res, abortController, params }) {
const rootSpan = web.root(req)
if (!rootSpan) return

if (!params || typeof params !== 'object' || !Object.keys(params).length) return

const results = waf.run({
persistent: {
[addresses.HTTP_INCOMING_COOKIES]: cookies
[addresses.HTTP_INCOMING_PARAMS]: params
}
}, req)

Expand All @@ -212,20 +238,6 @@ function onResponseBody ({ req, body }) {
}, req)
}

function onPassportVerify ({ credentials, user }) {
const store = storage.getStore()
const rootSpan = store?.req && web.root(store.req)

if (!rootSpan) {
log.warn('No rootSpan found in onPassportVerify')
return
}

passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
}

const responseAnalyzedSet = new WeakSet()

function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) {
// avoid "write after end" error
if (isBlocked(res)) {
Expand Down Expand Up @@ -287,14 +299,15 @@ function disable () {

// Channel#unsubscribe() is undefined for non active channels
if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator)
if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
if (queryParser.hasSubscribers) queryParser.unsubscribe(onRequestQueryParsed)
if (nextBodyParsed.hasSubscribers) nextBodyParsed.unsubscribe(onRequestBodyParsed)
if (nextQueryParsed.hasSubscribers) nextQueryParsed.unsubscribe(onRequestQueryParsed)
if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
if (expressProcessParams.hasSubscribers) expressProcessParams.unsubscribe(onRequestProcessParams)
if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader)
}
Expand Down
25 changes: 25 additions & 0 deletions packages/dd-trace/test/appsec/express-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@
],
"transformers": ["lowercase"],
"on_match": ["block"]
},
{
"id": "test-rule-id-2",
"name": "test-rule-name-2",
"tags": {
"type": "security_scanner",
"category": "attack_attempt"
},
"conditions": [
{
"parameters": {
"inputs": [
{
"address": "server.request.path_params"
}
],
"list": [
"testattack"
]
},
"operator": "phrase_match"
}
],
"transformers": ["lowercase"],
"on_match": ["block"]
}
]
}
Expand Down
Loading
Loading