Skip to content

Commit

Permalink
Exploit prevention monitoring ssrf (#4361)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Carles Capell <107924659+CarlesDD@users.noreply.github.com>
  • Loading branch information
uurien and CarlesDD authored Jun 4, 2024
1 parent e60feae commit 9b410b7
Show file tree
Hide file tree
Showing 12 changed files with 376 additions and 6 deletions.
3 changes: 3 additions & 0 deletions docs/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ tracer.init({
apiSecurity: {
enabled: true,
requestSampling: 1.0
},
rasp: {
enabled: true
}
}
});
Expand Down
9 changes: 9 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,15 @@ declare namespace tracer {
* @default 0.1
*/
requestSampling?: number
},
/**
* Configuration for RASP
*/
rasp?: {
/** Whether to enable RASP.
* @default false
*/
enabled?: boolean
}
};

Expand Down
6 changes: 4 additions & 2 deletions packages/dd-trace/src/appsec/addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ module.exports = {
HTTP_INCOMING_GRAPHQL_RESOLVERS: 'graphql.server.all_resolvers',
HTTP_INCOMING_GRAPHQL_RESOLVER: 'graphql.server.resolver',

HTTP_OUTGOING_BODY: 'server.response.body',
HTTP_INCOMING_RESPONSE_BODY: 'server.response.body',

HTTP_CLIENT_IP: 'http.client_ip',

USER_ID: 'usr.id',
WAF_CONTEXT_PROCESSOR: 'waf.context.processor'
WAF_CONTEXT_PROCESSOR: 'waf.context.processor',

HTTP_OUTGOING_URL: 'server.io.net.url'
}
3 changes: 2 additions & 1 deletion packages/dd-trace/src/appsec/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ module.exports = {
setCookieChannel: dc.channel('datadog:iast:set-cookie'),
nextBodyParsed: dc.channel('apm:next:body-parsed'),
nextQueryParsed: dc.channel('apm:next:query-parsed'),
responseBody: dc.channel('datadog:express:response:json:start')
responseBody: dc.channel('datadog:express:response:json:start'),
httpClientRequestStart: dc.channel('apm:http:client:request:start')
}
8 changes: 7 additions & 1 deletion packages/dd-trace/src/appsec/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const { block, setTemplates, getBlockingAction } = require('./blocking')
const { passportTrackEvent } = require('./passport')
const { storage } = require('../../../datadog-core')
const graphql = require('./graphql')
const rasp = require('./rasp')

let isEnabled = false
let config
Expand All @@ -37,6 +38,10 @@ function enable (_config) {
appsecTelemetry.enable(_config.telemetry)
graphql.enable()

if (_config.appsec.rasp.enabled) {
rasp.enable()
}

setTemplates(_config)

RuleManager.loadRules(_config.appsec)
Expand Down Expand Up @@ -203,7 +208,7 @@ function onResponseBody ({ req, body }) {
// we don't support blocking at this point, so no results needed
waf.run({
persistent: {
[addresses.HTTP_OUTGOING_BODY]: body
[addresses.HTTP_INCOMING_RESPONSE_BODY]: body
}
}, req)
}
Expand Down Expand Up @@ -237,6 +242,7 @@ function disable () {

appsecTelemetry.disable()
graphql.disable()
rasp.disable()

remoteConfig.disableWafUpdate()

Expand Down
35 changes: 35 additions & 0 deletions packages/dd-trace/src/appsec/rasp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict'

const { storage } = require('../../../datadog-core')
const addresses = require('./addresses')
const { httpClientRequestStart } = require('./channels')
const waf = require('./waf')

function enable () {
httpClientRequestStart.subscribe(analyzeSsrf)
}

function disable () {
if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
}

function analyzeSsrf (ctx) {
const store = storage.getStore()
const req = store?.req
const url = ctx.args.uri

if (!req || !url) return

const persistent = {
[addresses.HTTP_OUTGOING_URL]: url
}
// TODO: Currently this is only monitoring, we should
// block the request if SSRF attempt and
// generate stack traces
waf.run({ persistent }, req)
}

module.exports = {
enable,
disable
}
4 changes: 4 additions & 0 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ class Config {
this._setValue(defaults, 'appsec.enabled', undefined)
this._setValue(defaults, 'appsec.obfuscatorKeyRegex', defaultWafObfuscatorKeyRegex)
this._setValue(defaults, 'appsec.obfuscatorValueRegex', defaultWafObfuscatorValueRegex)
this._setValue(defaults, 'appsec.rasp.enabled', false)
this._setValue(defaults, 'appsec.rateLimit', 100)
this._setValue(defaults, 'appsec.rules', undefined)
this._setValue(defaults, 'appsec.sca.enabled', null)
Expand Down Expand Up @@ -527,6 +528,7 @@ class Config {
DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP,
DD_APPSEC_RULES,
DD_APPSEC_SCA_ENABLED,
DD_APPSEC_RASP_ENABLED,
DD_APPSEC_TRACE_RATE_LIMIT,
DD_APPSEC_WAF_TIMEOUT,
DD_DATA_STREAMS_ENABLED,
Expand Down Expand Up @@ -616,6 +618,7 @@ class Config {
this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED)
this._setString(env, 'appsec.obfuscatorKeyRegex', DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP)
this._setString(env, 'appsec.obfuscatorValueRegex', DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP)
this._setBoolean(env, 'appsec.rasp.enabled', DD_APPSEC_RASP_ENABLED)
this._setValue(env, 'appsec.rateLimit', maybeInt(DD_APPSEC_TRACE_RATE_LIMIT))
this._setString(env, 'appsec.rules', DD_APPSEC_RULES)
// DD_APPSEC_SCA_ENABLED is never used locally, but only sent to the backend
Expand Down Expand Up @@ -744,6 +747,7 @@ class Config {
this._setBoolean(opts, 'appsec.enabled', options.appsec.enabled)
this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec.obfuscatorKeyRegex)
this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec.obfuscatorValueRegex)
this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec.rasp?.enabled)
this._setValue(opts, 'appsec.rateLimit', maybeInt(options.appsec.rateLimit))
this._setString(opts, 'appsec.rules', options.appsec.rules)
this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec.wafTimeout))
Expand Down
28 changes: 26 additions & 2 deletions packages/dd-trace/test/appsec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('AppSec Index', () => {
let appsecTelemetry
let graphql
let apiSecuritySampler
let rasp

const RULES = { rules: [{ a: 1 }] }

Expand All @@ -63,6 +64,9 @@ describe('AppSec Index', () => {
apiSecurity: {
enabled: false,
requestSampling: 0
},
rasp: {
enabled: true
}
}
}
Expand Down Expand Up @@ -99,14 +103,20 @@ describe('AppSec Index', () => {
sinon.spy(apiSecuritySampler, 'sampleRequest')
sinon.spy(apiSecuritySampler, 'isSampled')

rasp = {
enable: sinon.stub(),
disable: sinon.stub()
}

AppSec = proxyquire('../../src/appsec', {
'../log': log,
'../plugins/util/web': web,
'./blocking': blocking,
'./passport': passport,
'./telemetry': appsecTelemetry,
'./graphql': graphql,
'./api_security_sampler': apiSecuritySampler
'./api_security_sampler': apiSecuritySampler,
'./rasp': rasp
})

sinon.stub(fs, 'readFileSync').returns(JSON.stringify(RULES))
Expand Down Expand Up @@ -183,6 +193,19 @@ describe('AppSec Index', () => {

expect(appsecTelemetry.enable).to.be.calledOnceWithExactly(config.telemetry)
})

it('should call rasp enable', () => {
AppSec.enable(config)

expect(rasp.enable).to.be.calledOnceWithExactly()
})

it('should not call rasp enable when rasp is disabled', () => {
config.appsec.rasp.enabled = false
AppSec.enable(config)

expect(rasp.enable).to.not.be.called
})
})

describe('disable', () => {
Expand All @@ -204,6 +227,7 @@ describe('AppSec Index', () => {
.to.have.been.calledOnceWithExactly(AppSec.incomingHttpStartTranslator)
expect(incomingHttpRequestEnd.unsubscribe).to.have.been.calledOnceWithExactly(AppSec.incomingHttpEndTranslator)
expect(graphql.disable).to.have.been.calledOnceWithExactly()
expect(rasp.disable).to.have.been.calledOnceWithExactly()
})

it('should disable AppSec when DC channels are not active', () => {
Expand Down Expand Up @@ -592,7 +616,7 @@ describe('AppSec Index', () => {
expect(apiSecuritySampler.isSampled).to.have.been.calledOnceWith(req)
expect(waf.run).to.been.calledOnceWith({
persistent: {
[addresses.HTTP_OUTGOING_BODY]: body
[addresses.HTTP_INCOMING_RESPONSE_BODY]: body
}
}, req)
})
Expand Down
116 changes: 116 additions & 0 deletions packages/dd-trace/test/appsec/rasp.express.plugin.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use strict'

const Axios = require('axios')
const agent = require('../plugins/agent')
const getPort = require('get-port')
const appsec = require('../../src/appsec')
const Config = require('../../src/config')
const path = require('path')
const { assert } = require('chai')

withVersions('express', 'express', expressVersion => {
describe('RASP', () => {
let app, server, port, axios

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

before((done) => {
const express = require(`../../../../versions/express@${expressVersion}`).get()
const expressApp = express()

expressApp.get('/', (req, res) => {
app(req, res)
})

appsec.enable(new Config({
appsec: {
enabled: true,
rules: path.join(__dirname, 'rasp_rules.json'),
rasp: { enabled: true }
}
}))

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

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

function getWebSpan (traces) {
for (const trace of traces) {
for (const span of trace) {
if (span.type === 'web') {
return span
}
}
}
throw new Error('web span not found')
}

describe('ssrf', () => {
['http', 'https'].forEach(protocol => {
describe(`Test using ${protocol}`, () => {
it('Should not detect threat', async () => {
app = (req, res) => {
require(protocol).get(`${protocol}://${req.query.host}`)
res.end('end')
}

axios.get('/?host=www.datadoghq.com')

await agent.use((traces) => {
const span = getWebSpan(traces)
assert.notProperty(span.meta, '_dd.appsec.json')
})
})

it('Should detect threat doing a GET request', async () => {
app = (req, res) => {
require(protocol).get(`${protocol}://${req.query.host}`)
res.end('end')
}

axios.get('/?host=ifconfig.pro')

await agent.use((traces) => {
const span = getWebSpan(traces)
assert.property(span.meta, '_dd.appsec.json')
assert(span.meta['_dd.appsec.json'].includes('rasp-ssrf-rule-id-1'))
})
})

it('Should detect threat doing a POST request', async () => {
app = (req, res) => {
const clientRequest = require(protocol)
.request(`${protocol}://${req.query.host}`, { method: 'POST' })
clientRequest.write('dummy_post_data')
clientRequest.end()
res.end('end')
}

axios.get('/?host=ifconfig.pro')

await agent.use((traces) => {
const span = getWebSpan(traces)
assert.property(span.meta, '_dd.appsec.json')
assert(span.meta['_dd.appsec.json'].includes('rasp-ssrf-rule-id-1'))
})
})
})
})
})
})
})
Loading

0 comments on commit 9b410b7

Please sign in to comment.