Skip to content

Commit b4bb557

Browse files
CarlesDDtlhunter
authored andcommitted
Taint cookies and headers (#3232)
* Taint cookies and headers * Bump minimum node version for v4 on cookie plugin test * Add test with latest node version for cookie plugin test * Provide iastContext from index when tainting headers * Add test for cookie tainting in taint tracking plugin * Remove iast transaction after taint tracking plugin tests to avoid hiting setMaxTransactions in tests * Add test for taintObject with taintingKeys flag * Address header tainting test for keys shorter than 10 chars * Upgrade native-iast-taint-tracking to v1.5.0 * Rewrite expect in taint tracking plugin test * Fix tag requiring in IAST index
1 parent acca3df commit b4bb557

File tree

14 files changed

+320
-22
lines changed

14 files changed

+320
-22
lines changed

.github/workflows/appsec.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,18 @@ jobs:
136136
- uses: ./.github/actions/node/latest
137137
- run: yarn test:appsec:plugins:ci
138138
- uses: codecov/codecov-action@v2
139+
140+
sourcing:
141+
runs-on: ubuntu-latest
142+
env:
143+
PLUGINS: cookie
144+
steps:
145+
- uses: actions/checkout@v2
146+
- uses: ./.github/actions/node/setup
147+
- run: yarn install
148+
- uses: ./.github/actions/node/16
149+
- run: yarn test:appsec:plugins:ci
150+
- uses: ./.github/actions/node/18
151+
- run: yarn test:appsec:plugins:ci
152+
- uses: ./.github/actions/node/latest
153+
- run: yarn test:appsec:plugins:ci

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"dependencies": {
6969
"@datadog/native-appsec": "^3.2.0",
7070
"@datadog/native-iast-rewriter": "2.0.1",
71-
"@datadog/native-iast-taint-tracking": "^1.4.1",
71+
"@datadog/native-iast-taint-tracking": "^1.5.0",
7272
"@datadog/native-metrics": "^1.6.0",
7373
"@datadog/pprof": "^2.2.1",
7474
"@datadog/sketches-js": "^2.1.0",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
const shimmer = require('../../datadog-shimmer')
4+
const { channel, addHook } = require('./helpers/instrument')
5+
6+
const cookieParseCh = channel('datadog:cookie:parse:finish')
7+
8+
function wrapParse (originalParse) {
9+
return function () {
10+
const cookies = originalParse.apply(this, arguments)
11+
if (cookieParseCh.hasSubscribers && cookies) {
12+
cookieParseCh.publish({ cookies })
13+
}
14+
return cookies
15+
}
16+
}
17+
18+
addHook({ name: 'cookie', versions: ['>=0.4'] }, cookie => {
19+
shimmer.wrap(cookie, 'parse', wrapParse)
20+
return cookie
21+
})

packages/datadog-instrumentations/src/helpers/hooks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module.exports = {
2525
'child_process': () => require('../child-process'),
2626
'node:child_process': () => require('../child-process'),
2727
'connect': () => require('../connect'),
28+
'cookie': () => require('../cookie'),
2829
'couchbase': () => require('../couchbase'),
2930
'crypto': () => require('../crypto'),
3031
'cypress': () => require('../cypress'),

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ const { storage } = require('../../../../datadog-core')
55
const overheadController = require('./overhead-controller')
66
const dc = require('../../../../diagnostics_channel')
77
const iastContextFunctions = require('./iast-context')
8-
const { enableTaintTracking, disableTaintTracking, createTransaction, removeTransaction } = require('./taint-tracking')
8+
const {
9+
enableTaintTracking,
10+
disableTaintTracking,
11+
createTransaction,
12+
removeTransaction,
13+
taintTrackingPlugin
14+
} = require('./taint-tracking')
915
const { IAST_ENABLED_TAG_KEY } = require('./tags')
1016

1117
const telemetryLogs = require('./telemetry/logs')
@@ -48,6 +54,7 @@ function onIncomingHttpRequestStart (data) {
4854
const iastContext = iastContextFunctions.saveIastContext(store, topContext, { rootSpan, req: data.req })
4955
createTransaction(rootSpan.context().toSpanId(), iastContext)
5056
overheadController.initializeRequestContext(iastContext)
57+
taintTrackingPlugin.taintHeaders(data.req.headers, iastContext)
5158
}
5259
if (rootSpan.addTags) {
5360
rootSpan.addTags({

packages/dd-trace/src/appsec/iast/taint-tracking/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ module.exports = {
2323
},
2424
setMaxTransactions: setMaxTransactions,
2525
createTransaction: createTransaction,
26-
removeTransaction: removeTransaction
26+
removeTransaction: removeTransaction,
27+
taintTrackingPlugin
2728
}

packages/dd-trace/src/appsec/iast/taint-tracking/operations.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ function newTaintedString (iastContext, string, name, type) {
3030
return result
3131
}
3232

33-
function taintObject (iastContext, object, type) {
33+
function taintObject (iastContext, object, type, keyTainting, keyType) {
3434
let result = object
3535
if (iastContext && iastContext[IAST_TRANSACTION_ID]) {
3636
const transactionId = iastContext[IAST_TRANSACTION_ID]
3737
const queue = [{ parent: null, property: null, value: object }]
3838
const visited = new WeakSet()
3939
while (queue.length > 0) {
40-
const { parent, property, value } = queue.pop()
40+
const { parent, property, value, key } = queue.pop()
4141
if (value === null) {
4242
continue
4343
}
@@ -47,14 +47,23 @@ function taintObject (iastContext, object, type) {
4747
if (!parent) {
4848
result = tainted
4949
} else {
50-
parent[property] = tainted
50+
if (keyTainting && key) {
51+
const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
52+
parent[taintedProperty] = tainted
53+
} else {
54+
parent[property] = tainted
55+
}
5156
}
5257
} else if (typeof value === 'object' && !visited.has(value)) {
5358
visited.add(value)
5459
const keys = Object.keys(value)
5560
for (let i = 0; i < keys.length; i++) {
5661
const key = keys[i]
57-
queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key] })
62+
queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key], key })
63+
}
64+
if (parent && keyTainting && key) {
65+
const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
66+
parent[taintedProperty] = value
5867
}
5968
}
6069
} catch (e) {

packages/dd-trace/src/appsec/iast/taint-tracking/origin-types.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
module.exports = {
44
HTTP_REQUEST_BODY: 'http.request.body',
55
HTTP_REQUEST_PARAMETER: 'http.request.parameter',
6+
HTTP_REQUEST_COOKIE_VALUE: 'http.request.cookie.value',
7+
HTTP_REQUEST_COOKIE_NAME: 'http.request.cookie.name',
8+
HTTP_REQUEST_HEADER_NAME: 'http.request.header.name',
69
HTTP_REQUEST_HEADER_VALUE: 'http.request.header'
710
}

packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33
const Plugin = require('../../../plugins/plugin')
44
const { getIastContext } = require('../iast-context')
55
const { storage } = require('../../../../../datadog-core')
6-
const { HTTP_REQUEST_PARAMETER, HTTP_REQUEST_BODY } = require('./origin-types')
76
const { taintObject } = require('./operations')
7+
const {
8+
HTTP_REQUEST_PARAMETER,
9+
HTTP_REQUEST_BODY,
10+
HTTP_REQUEST_COOKIE_VALUE,
11+
HTTP_REQUEST_COOKIE_NAME,
12+
HTTP_REQUEST_HEADER_VALUE,
13+
HTTP_REQUEST_HEADER_NAME
14+
} = require('./origin-types')
815

916
class TaintTrackingPlugin extends Plugin {
1017
constructor () {
@@ -22,8 +29,8 @@ class TaintTrackingPlugin extends Plugin {
2229
)
2330
this.addSub(
2431
'datadog:qs:parse:finish',
25-
({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs))
26-
32+
({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs)
33+
)
2734
this.addSub('apm:express:middleware:next', ({ req }) => {
2835
if (req && req.body && typeof req.body === 'object') {
2936
const iastContext = getIastContext(storage.getStore())
@@ -33,16 +40,29 @@ class TaintTrackingPlugin extends Plugin {
3340
}
3441
}
3542
})
43+
this.addSub(
44+
'datadog:cookie:parse:finish',
45+
({ cookies }) => this._cookiesTaintTrackingHandler(cookies)
46+
)
3647
}
3748

3849
_taintTrackingHandler (type, target, property, iastContext = getIastContext(storage.getStore())) {
3950
if (!property) {
4051
taintObject(iastContext, target, type)
41-
} else {
52+
} else if (target[property]) {
4253
target[property] = taintObject(iastContext, target[property], type)
4354
}
4455
}
4556

57+
_cookiesTaintTrackingHandler (target) {
58+
const iastContext = getIastContext(storage.getStore())
59+
taintObject(iastContext, target, HTTP_REQUEST_COOKIE_VALUE, true, HTTP_REQUEST_COOKIE_NAME)
60+
}
61+
62+
taintHeaders (headers, iastContext) {
63+
taintObject(iastContext, headers, HTTP_REQUEST_HEADER_VALUE, true, HTTP_REQUEST_HEADER_NAME)
64+
}
65+
4666
enable () {
4767
this.configure(true)
4868
}

packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ const proxyquire = require('proxyquire')
44
const iastContextFunctions = require('../../../../src/appsec/iast/iast-context')
55
const taintTrackingOperations = require('../../../../src/appsec/iast/taint-tracking/operations')
66
const dc = require('../../../../../diagnostics_channel')
7+
const {
8+
HTTP_REQUEST_COOKIE_VALUE,
9+
HTTP_REQUEST_COOKIE_NAME
10+
} = require('../../../../src/appsec/iast/taint-tracking/origin-types')
711

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

1217
describe('IAST Taint tracking plugin', () => {
1318
let taintTrackingPlugin
@@ -33,11 +38,12 @@ describe('IAST Taint tracking plugin', () => {
3338
sinon.restore()
3439
})
3540

36-
it('Should subscribe to body parser and qs channel', () => {
37-
expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(3)
41+
it('Should subscribe to body parser, qs and cookie channel', () => {
42+
expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(4)
3843
expect(taintTrackingPlugin._subscriptions[0]._channel.name).to.equals('datadog:body-parser:read:finish')
3944
expect(taintTrackingPlugin._subscriptions[1]._channel.name).to.equals('datadog:qs:parse:finish')
4045
expect(taintTrackingPlugin._subscriptions[2]._channel.name).to.equals('apm:express:middleware:next')
46+
expect(taintTrackingPlugin._subscriptions[3]._channel.name).to.equals('datadog:cookie:parse:finish')
4147
})
4248

4349
describe('taint sources', () => {
@@ -55,6 +61,10 @@ describe('IAST Taint tracking plugin', () => {
5561
)
5662
})
5763

64+
afterEach(() => {
65+
taintTrackingOperations.removeTransaction(iastContext)
66+
})
67+
5868
it('Should taint full object', () => {
5969
const originType = 'ORIGIN_TYPE'
6070
const objToBeTainted = {
@@ -108,11 +118,7 @@ describe('IAST Taint tracking plugin', () => {
108118
}
109119

110120
taintTrackingPlugin._taintTrackingHandler(originType, objToBeTainted, propertyToBeTainted)
111-
expect(taintTrackingOperations.taintObject).to.be.calledOnceWith(
112-
iastContext,
113-
objToBeTainted[propertyToBeTainted],
114-
originType
115-
)
121+
expect(taintTrackingOperations.taintObject).not.to.be.called
116122
})
117123

118124
it('Should taint request parameter when qs event is published', () => {
@@ -180,5 +186,21 @@ describe('IAST Taint tracking plugin', () => {
180186
'http.request.body'
181187
)
182188
})
189+
190+
it('Should taint cookies when cookie parser event is published', () => {
191+
const cookies = {
192+
cookie1: 'tainted_cookie'
193+
}
194+
195+
cookieParseFinishCh.publish({ cookies })
196+
197+
expect(taintTrackingOperations.taintObject).to.be.calledOnceWith(
198+
iastContext,
199+
cookies,
200+
HTTP_REQUEST_COOKIE_VALUE,
201+
true,
202+
HTTP_REQUEST_COOKIE_NAME
203+
)
204+
})
183205
})
184206
})

0 commit comments

Comments
 (0)