Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/appsec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,18 @@ jobs:
- uses: ./.github/actions/node/latest
- run: yarn test:appsec:plugins:ci
- uses: codecov/codecov-action@v2

sourcing:
runs-on: ubuntu-latest
env:
PLUGINS: cookie
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/node/setup
- run: yarn install
- uses: ./.github/actions/node/16
- run: yarn test:appsec:plugins:ci
- uses: ./.github/actions/node/18
- run: yarn test:appsec:plugins:ci
- uses: ./.github/actions/node/latest
- run: yarn test:appsec:plugins:ci
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"dependencies": {
"@datadog/native-appsec": "^3.2.0",
"@datadog/native-iast-rewriter": "2.0.1",
"@datadog/native-iast-taint-tracking": "^1.4.1",
"@datadog/native-iast-taint-tracking": "^1.5.0",
"@datadog/native-metrics": "^2.0.0",
"@datadog/pprof": "^2.2.1",
"@datadog/sketches-js": "^2.1.0",
Expand Down
21 changes: 21 additions & 0 deletions packages/datadog-instrumentations/src/cookie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict'

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

const cookieParseCh = channel('datadog:cookie:parse:finish')

function wrapParse (originalParse) {
return function () {
const cookies = originalParse.apply(this, arguments)
if (cookieParseCh.hasSubscribers && cookies) {
cookieParseCh.publish({ cookies })
}
return cookies
}
}

addHook({ name: 'cookie', versions: ['>=0.4'] }, cookie => {
shimmer.wrap(cookie, 'parse', wrapParse)
return cookie
})
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 @@ -25,6 +25,7 @@ module.exports = {
'child_process': () => require('../child-process'),
'node:child_process': () => require('../child-process'),
'connect': () => require('../connect'),
'cookie': () => require('../cookie'),
'couchbase': () => require('../couchbase'),
'crypto': () => require('../crypto'),
'cypress': () => require('../cypress'),
Expand Down
9 changes: 8 additions & 1 deletion packages/dd-trace/src/appsec/iast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ const { storage } = require('../../../../datadog-core')
const overheadController = require('./overhead-controller')
const dc = require('../../../../diagnostics_channel')
const iastContextFunctions = require('./iast-context')
const { enableTaintTracking, disableTaintTracking, createTransaction, removeTransaction } = require('./taint-tracking')
const {
enableTaintTracking,
disableTaintTracking,
createTransaction,
removeTransaction,
taintTrackingPlugin
} = require('./taint-tracking')
const { IAST_ENABLED_TAG_KEY } = require('./tags')

const telemetryLogs = require('./telemetry/logs')
Expand Down Expand Up @@ -48,6 +54,7 @@ function onIncomingHttpRequestStart (data) {
const iastContext = iastContextFunctions.saveIastContext(store, topContext, { rootSpan, req: data.req })
createTransaction(rootSpan.context().toSpanId(), iastContext)
overheadController.initializeRequestContext(iastContext)
taintTrackingPlugin.taintHeaders(data.req.headers, iastContext)
}
if (rootSpan.addTags) {
rootSpan.addTags({
Expand Down
3 changes: 2 additions & 1 deletion packages/dd-trace/src/appsec/iast/taint-tracking/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ module.exports = {
},
setMaxTransactions: setMaxTransactions,
createTransaction: createTransaction,
removeTransaction: removeTransaction
removeTransaction: removeTransaction,
taintTrackingPlugin
}
17 changes: 13 additions & 4 deletions packages/dd-trace/src/appsec/iast/taint-tracking/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ function newTaintedString (iastContext, string, name, type) {
return result
}

function taintObject (iastContext, object, type) {
function taintObject (iastContext, object, type, keyTainting, keyType) {
let result = object
if (iastContext && iastContext[IAST_TRANSACTION_ID]) {
const transactionId = iastContext[IAST_TRANSACTION_ID]
const queue = [{ parent: null, property: null, value: object }]
const visited = new WeakSet()
while (queue.length > 0) {
const { parent, property, value } = queue.pop()
const { parent, property, value, key } = queue.pop()
if (value === null) {
continue
}
Expand All @@ -47,14 +47,23 @@ function taintObject (iastContext, object, type) {
if (!parent) {
result = tainted
} else {
parent[property] = tainted
if (keyTainting && key) {
const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
parent[taintedProperty] = tainted
} else {
parent[property] = tainted
}
}
} else if (typeof value === 'object' && !visited.has(value)) {
visited.add(value)
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key] })
queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key], key })
}
if (parent && keyTainting && key) {
const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
parent[taintedProperty] = value
}
}
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
module.exports = {
HTTP_REQUEST_BODY: 'http.request.body',
HTTP_REQUEST_PARAMETER: 'http.request.parameter',
HTTP_REQUEST_COOKIE_VALUE: 'http.request.cookie.value',
HTTP_REQUEST_COOKIE_NAME: 'http.request.cookie.name',
HTTP_REQUEST_HEADER_NAME: 'http.request.header.name',
HTTP_REQUEST_HEADER_VALUE: 'http.request.header'
}
28 changes: 24 additions & 4 deletions packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
const Plugin = require('../../../plugins/plugin')
const { getIastContext } = require('../iast-context')
const { storage } = require('../../../../../datadog-core')
const { HTTP_REQUEST_PARAMETER, HTTP_REQUEST_BODY } = require('./origin-types')
const { taintObject } = require('./operations')
const {
HTTP_REQUEST_PARAMETER,
HTTP_REQUEST_BODY,
HTTP_REQUEST_COOKIE_VALUE,
HTTP_REQUEST_COOKIE_NAME,
HTTP_REQUEST_HEADER_VALUE,
HTTP_REQUEST_HEADER_NAME
} = require('./origin-types')

class TaintTrackingPlugin extends Plugin {
constructor () {
Expand All @@ -22,8 +29,8 @@ class TaintTrackingPlugin extends Plugin {
)
this.addSub(
'datadog:qs:parse:finish',
({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs))

({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs)
)
this.addSub('apm:express:middleware:next', ({ req }) => {
if (req && req.body && typeof req.body === 'object') {
const iastContext = getIastContext(storage.getStore())
Expand All @@ -33,16 +40,29 @@ class TaintTrackingPlugin extends Plugin {
}
}
})
this.addSub(
'datadog:cookie:parse:finish',
({ cookies }) => this._cookiesTaintTrackingHandler(cookies)
)
}

_taintTrackingHandler (type, target, property, iastContext = getIastContext(storage.getStore())) {
if (!property) {
taintObject(iastContext, target, type)
} else {
} else if (target[property]) {
target[property] = taintObject(iastContext, target[property], type)
}
}

_cookiesTaintTrackingHandler (target) {
const iastContext = getIastContext(storage.getStore())
taintObject(iastContext, target, HTTP_REQUEST_COOKIE_VALUE, true, HTTP_REQUEST_COOKIE_NAME)
}

taintHeaders (headers, iastContext) {
taintObject(iastContext, headers, HTTP_REQUEST_HEADER_VALUE, true, HTTP_REQUEST_HEADER_NAME)
}

enable () {
this.configure(true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ const proxyquire = require('proxyquire')
const iastContextFunctions = require('../../../../src/appsec/iast/iast-context')
const taintTrackingOperations = require('../../../../src/appsec/iast/taint-tracking/operations')
const dc = require('../../../../../diagnostics_channel')
const {
HTTP_REQUEST_COOKIE_VALUE,
HTTP_REQUEST_COOKIE_NAME
} = require('../../../../src/appsec/iast/taint-tracking/origin-types')

const middlewareNextChannel = dc.channel('apm:express:middleware:next')
const queryParseFinishChannel = dc.channel('datadog:qs:parse:finish')
const bodyParserFinishChannel = dc.channel('datadog:body-parser:read:finish')
const cookieParseFinishCh = dc.channel('datadog:cookie:parse:finish')

describe('IAST Taint tracking plugin', () => {
let taintTrackingPlugin
Expand All @@ -33,11 +38,12 @@ describe('IAST Taint tracking plugin', () => {
sinon.restore()
})

it('Should subscribe to body parser and qs channel', () => {
expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(3)
it('Should subscribe to body parser, qs and cookie channel', () => {
expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(4)
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')
})

describe('taint sources', () => {
Expand All @@ -55,6 +61,10 @@ describe('IAST Taint tracking plugin', () => {
)
})

afterEach(() => {
taintTrackingOperations.removeTransaction(iastContext)
})

it('Should taint full object', () => {
const originType = 'ORIGIN_TYPE'
const objToBeTainted = {
Expand Down Expand Up @@ -108,11 +118,7 @@ describe('IAST Taint tracking plugin', () => {
}

taintTrackingPlugin._taintTrackingHandler(originType, objToBeTainted, propertyToBeTainted)
expect(taintTrackingOperations.taintObject).to.be.calledOnceWith(
iastContext,
objToBeTainted[propertyToBeTainted],
originType
)
expect(taintTrackingOperations.taintObject).not.to.be.called
})

it('Should taint request parameter when qs event is published', () => {
Expand Down Expand Up @@ -180,5 +186,21 @@ describe('IAST Taint tracking plugin', () => {
'http.request.body'
)
})

it('Should taint cookies when cookie parser event is published', () => {
const cookies = {
cookie1: 'tainted_cookie'
}

cookieParseFinishCh.publish({ cookies })

expect(taintTrackingOperations.taintObject).to.be.calledOnceWith(
iastContext,
cookies,
HTTP_REQUEST_COOKIE_VALUE,
true,
HTTP_REQUEST_COOKIE_NAME
)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict'

const axios = require('axios')
const Config = require('../../../../../src/config')
const { storage } = require('../../../../../../datadog-core')
const iast = require('../../../../../src/appsec/iast')
const iastContextFunctions = require('../../../../../src/appsec/iast/iast-context')
const { isTainted, getRanges } = require('../../../../../src/appsec/iast/taint-tracking/operations')
const {
HTTP_REQUEST_COOKIE_NAME,
HTTP_REQUEST_COOKIE_VALUE
} = require('../../../../../src/appsec/iast/taint-tracking/origin-types')
const { testInRequest } = require('../../utils')

describe('Cookies sourcing with cookies', () => {
let cookie
withVersions('cookie', 'cookie', version => {
function app () {
const store = storage.getStore()
const iastContext = iastContextFunctions.getIastContext(store)

const rawCookies = 'cookie=value'
const parsedCookies = cookie.parse(rawCookies)
Object.getOwnPropertySymbols(parsedCookies).forEach(cookieName => {
const cookieValue = parsedCookies[cookieName]
const isCookieValueTainted = isTainted(iastContext, cookieValue)
expect(isCookieValueTainted).to.be.true
const taintedCookieValueRanges = getRanges(iastContext, cookieValue)
expect(taintedCookieValueRanges[0].iinfo.type).to.be.equal(HTTP_REQUEST_COOKIE_VALUE)
const isCookieNameTainted = isTainted(iastContext, cookieName)
expect(isCookieNameTainted).to.be.true
const taintedCookieNameRanges = getRanges(iastContext, cookieName)
expect(taintedCookieNameRanges[0].iinfo.type).to.be.equal(HTTP_REQUEST_COOKIE_NAME)
})
}

function tests (config) {
beforeEach(() => {
iast.enable(new Config({
experimental: {
iast: {
enabled: true,
requestSampling: 100
}
}
}))

cookie = require(`../../../../../../../versions/cookie@${version}`).get()
})

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

it('should taint cookies', (done) => {
axios.get(`http://localhost:${config.port}/`)
.then(() => done())
.catch(done)
})
}

testInRequest(app, tests)
})
})
Loading