Skip to content

Commit b053322

Browse files
committed
support fastify iast
1 parent 030dad4 commit b053322

File tree

8 files changed

+430
-41
lines changed

8 files changed

+430
-41
lines changed

packages/datadog-instrumentations/src/http/server.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ function wrapWriteHead (writeHead) {
112112
// this doesn't support explicit duplicate headers, but it's an edge case
113113
const responseHeaders = Object.assign(this.getHeaders(), obj)
114114

115+
// Publish all headers from writeHead to ensure frameworks like Fastify
116+
// that bypass setHeader() are still captured for analysis
117+
if (finishSetHeaderCh.hasSubscribers) {
118+
for (const [name, value] of Object.entries(responseHeaders)) {
119+
finishSetHeaderCh.publish({ name, value, res: this })
120+
}
121+
}
122+
115123
startWriteHeadCh.publish({
116124
req: this.req,
117125
res: this,

packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
const Analyzer = require('./vulnerability-analyzer')
44
const { getNodeModulesPaths } = require('../path-line')
55

6-
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
6+
const EXCLUDED_PATHS = [
7+
getNodeModulesPaths('express/lib/response.js'),
8+
getNodeModulesPaths('fastify/lib/reply.js'),
9+
getNodeModulesPaths('fastify/lib/hooks.js'),
10+
getNodeModulesPaths('@fastify/cookie/plugin.js'),
11+
]
712

813
class CookieAnalyzer extends Analyzer {
914
constructor (type, propertyToBeSafe) {

packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class Analyzer extends SinkIastPlugin {
3939

4040
const location = this._getLocation(value, nonDDCallSiteFrames)
4141

42+
if (this._type === "INSECURE_COOKIE") {
43+
console.log('======>>> callSiteFrames ', callSiteFrames)
44+
console.log('======>>> nonDDCallSiteFrames ', nonDDCallSiteFrames)
45+
console.log('======>>> location ', location)
46+
}
47+
48+
4249
if (!this._isExcluded(location)) {
4350
const originalLocation = this._getOriginalLocation(location)
4451
const spanId = context?.rootSpan?.context().toSpanId()

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

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ class TaintTrackingPlugin extends SourceIastPlugin {
3838
}
3939

4040
onConfigure () {
41+
this.addBodyParsingSubscriptions()
42+
43+
this.addQueryParameterSubscriptions()
44+
45+
this.addCookieSubscriptions()
46+
47+
this.addDatabaseSubscriptions()
48+
49+
this.addPathParameterSubscriptions()
50+
51+
this.addGraphQLSubscriptions()
52+
53+
this.addURLParsingSubscriptions()
54+
55+
// this is a special case to increment INSTRUMENTED_SOURCE metric for header
56+
this.addInstrumentedSource('http', [HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_HEADER_NAME])
57+
}
58+
59+
addBodyParsingSubscriptions () {
4160
const onRequestBody = ({ req }) => {
4261
const iastContext = getIastContext(storage('legacy').getStore())
4362
if (iastContext && iastContext.body !== req.body) {
@@ -57,17 +76,14 @@ class TaintTrackingPlugin extends SourceIastPlugin {
5776
)
5877

5978
this.addSub(
60-
{ channelName: 'datadog:query:read:finish', tag: HTTP_REQUEST_PARAMETER },
61-
({ query }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query)
62-
)
63-
64-
this.addSub(
65-
{ channelName: 'datadog:express:query:finish', tag: HTTP_REQUEST_PARAMETER },
66-
({ query }) => {
79+
{ channelName: 'datadog:fastify:body-parser:finish', tag: HTTP_REQUEST_BODY },
80+
({ req, body }) => {
6781
const iastContext = getIastContext(storage('legacy').getStore())
68-
if (!iastContext || !query) return
69-
70-
taintQueryWithCache(iastContext, query)
82+
if (iastContext && iastContext.body !== body) {
83+
req.body = body
84+
this._taintTrackingHandler(HTTP_REQUEST_BODY, req, 'body', iastContext)
85+
iastContext.body = body
86+
}
7187
}
7288
)
7389

@@ -83,12 +99,45 @@ class TaintTrackingPlugin extends SourceIastPlugin {
8399
}
84100
}
85101
)
102+
}
86103

104+
addQueryParameterSubscriptions () {
105+
this.addSub(
106+
{ channelName: 'datadog:query:read:finish', tag: HTTP_REQUEST_PARAMETER },
107+
({ query }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query)
108+
)
109+
110+
this.addSub(
111+
{ channelName: 'datadog:fastify:query-params:finish', tag: HTTP_REQUEST_PARAMETER },
112+
({ query }) => {
113+
this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query)
114+
}
115+
)
116+
117+
this.addSub(
118+
{ channelName: 'datadog:express:query:finish', tag: HTTP_REQUEST_PARAMETER },
119+
({ query }) => {
120+
const iastContext = getIastContext(storage('legacy').getStore())
121+
if (!iastContext || !query) return
122+
123+
taintQueryWithCache(iastContext, query)
124+
}
125+
)
126+
}
127+
128+
addCookieSubscriptions () {
87129
this.addSub(
88130
{ channelName: 'datadog:cookie:parse:finish', tag: [HTTP_REQUEST_COOKIE_VALUE, HTTP_REQUEST_COOKIE_NAME] },
89131
({ cookies }) => this._cookiesTaintTrackingHandler(cookies)
90132
)
91133

134+
this.addSub(
135+
{ channelName: 'datadog:fastify-cookie:read:finish', tag: [HTTP_REQUEST_COOKIE_VALUE, HTTP_REQUEST_COOKIE_NAME] },
136+
({ cookies }) => this._cookiesTaintTrackingHandler(cookies)
137+
)
138+
}
139+
140+
addDatabaseSubscriptions () {
92141
this.addSub(
93142
{ channelName: 'datadog:sequelize:query:finish', tag: SQL_ROW_VALUE },
94143
({ result }) => this._taintDatabaseResult(result, 'sequelize')
@@ -98,25 +147,32 @@ class TaintTrackingPlugin extends SourceIastPlugin {
98147
{ channelName: 'apm:pg:query:finish', tag: SQL_ROW_VALUE },
99148
({ result }) => this._taintDatabaseResult(result, 'pg')
100149
)
150+
}
151+
152+
addPathParameterSubscriptions () {
153+
const pathParamHandler = ({ req }) => {
154+
if (req && req.params !== null && typeof req.params === 'object') {
155+
this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params')
156+
}
157+
}
101158

102159
this.addSub(
103160
{ channelName: 'datadog:express:process_params:start', tag: HTTP_REQUEST_PATH_PARAM },
104-
({ req }) => {
105-
if (req && req.params !== null && typeof req.params === 'object') {
106-
this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params')
107-
}
108-
}
161+
pathParamHandler
162+
)
163+
164+
this.addSub(
165+
{ channelName: 'datadog:fastify:path-params:finish', tag: HTTP_REQUEST_PATH_PARAM },
166+
pathParamHandler
109167
)
110168

111169
this.addSub(
112170
{ channelName: 'datadog:router:param:start', tag: HTTP_REQUEST_PATH_PARAM },
113-
({ req }) => {
114-
if (req && req.params !== null && typeof req.params === 'object') {
115-
this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params')
116-
}
117-
}
171+
pathParamHandler
118172
)
173+
}
119174

175+
addGraphQLSubscriptions () {
120176
this.addSub(
121177
{ channelName: 'apm:graphql:resolve:start', tag: HTTP_REQUEST_BODY },
122178
(data) => {
@@ -128,7 +184,9 @@ class TaintTrackingPlugin extends SourceIastPlugin {
128184
}
129185
}
130186
)
187+
}
131188

189+
addURLParsingSubscriptions () {
132190
const urlResultTaintedProperties = ['host', 'origin', 'hostname']
133191
this.addSub(
134192
{ channelName: 'datadog:url:parse:finish' },
@@ -162,9 +220,6 @@ class TaintTrackingPlugin extends SourceIastPlugin {
162220
context.result =
163221
newTaintedString(iastContext, context.result, origRange.iinfo.parameterName, origRange.iinfo.type)
164222
})
165-
166-
// this is a special case to increment INSTRUMENTED_SOURCE metric for header
167-
this.addInstrumentedSource('http', [HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_HEADER_NAME])
168223
}
169224

170225
_taintTrackingHandler (type, target, property, iastContext = getIastContext(storage('legacy').getStore())) {
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
'use strict'
2+
3+
const { prepareTestServerForIastInFastify } = require('../utils')
4+
const axios = require('axios')
5+
const { URL } = require('url')
6+
7+
function noop () {}
8+
9+
describe('Taint tracking plugin sources fastify tests', () => {
10+
withVersions('fastify', 'fastify', '>=2', version => {
11+
prepareTestServerForIastInFastify('in fastify', version,
12+
(testThatRequestHasVulnerability, _, config) => {
13+
describe.skip('tainted body', () => {
14+
function makePostRequest (done) {
15+
axios.post(`http://localhost:${config.port}/`, {
16+
command: 'echo 1'
17+
}).catch(done)
18+
}
19+
20+
testThatRequestHasVulnerability((req) => {
21+
const childProcess = require('child_process')
22+
childProcess.exec(req.body.command, noop)
23+
}, 'COMMAND_INJECTION', 1, noop, makePostRequest)
24+
})
25+
26+
describe.skip('tainted query param', () => {
27+
function makeRequestWithQueryParam (done) {
28+
axios.get(`http://localhost:${config.port}/?command=echo`).catch(done)
29+
}
30+
31+
testThatRequestHasVulnerability((req) => {
32+
const childProcess = require('child_process')
33+
childProcess.exec(req.query.command, noop)
34+
}, 'COMMAND_INJECTION', 1, noop, makeRequestWithQueryParam)
35+
})
36+
37+
describe.skip('tainted header', () => {
38+
function makeRequestWithHeader (done) {
39+
axios.get(`http://localhost:${config.port}/`, {
40+
headers: {
41+
'x-iast-test-command': 'echo 1'
42+
}
43+
}).catch(done)
44+
}
45+
46+
testThatRequestHasVulnerability((req) => {
47+
const childProcess = require('child_process')
48+
childProcess.exec(req.headers['x-iast-test-command'], noop)
49+
}, 'COMMAND_INJECTION', 1, noop, makeRequestWithHeader)
50+
})
51+
52+
describe.skip('url parse taint tracking', () => {
53+
function makePostRequest (done) {
54+
axios.post(`http://localhost:${config.port}/`, {
55+
url: 'http://www.datadoghq.com/'
56+
}).catch(done)
57+
}
58+
59+
testThatRequestHasVulnerability(
60+
{
61+
fn: (req) => {
62+
// eslint-disable-next-line n/no-deprecated-api
63+
const { parse } = require('url')
64+
const url = parse(req.body.url)
65+
66+
const childProcess = require('child_process')
67+
childProcess.exec(url.host, noop)
68+
},
69+
vulnerability: 'COMMAND_INJECTION',
70+
occurrences: 1,
71+
cb: noop,
72+
makeRequest: makePostRequest,
73+
testDescription: 'should detect vulnerability when tainted is coming from url.parse'
74+
})
75+
76+
testThatRequestHasVulnerability(
77+
{
78+
fn: (req) => {
79+
const { URL } = require('url')
80+
const url = new URL(req.body.url)
81+
82+
const childProcess = require('child_process')
83+
childProcess.exec(url.host, noop)
84+
},
85+
vulnerability: 'COMMAND_INJECTION',
86+
occurrences: 1,
87+
cb: noop,
88+
makeRequest: makePostRequest,
89+
testDescription: 'should detect vulnerability when tainted is coming from new url.URL input'
90+
})
91+
92+
testThatRequestHasVulnerability(
93+
{
94+
fn: (req) => {
95+
const { URL } = require('url')
96+
const url = new URL('/path', req.body.url)
97+
98+
const childProcess = require('child_process')
99+
childProcess.exec(url.host, noop)
100+
},
101+
vulnerability: 'COMMAND_INJECTION',
102+
occurrences: 1,
103+
cb: noop,
104+
makeRequest: makePostRequest,
105+
testDescription: 'should detect vulnerability when tainted is coming from new url.URL base'
106+
})
107+
108+
if (URL.parse) {
109+
testThatRequestHasVulnerability(
110+
{
111+
fn: (req) => {
112+
const { URL } = require('url')
113+
const url = URL.parse(req.body.url)
114+
const childProcess = require('child_process')
115+
childProcess.exec(url.host, noop)
116+
},
117+
vulnerability: 'COMMAND_INJECTION',
118+
occurrences: 1,
119+
cb: noop,
120+
makeRequest: makePostRequest,
121+
testDescription: 'should detect vulnerability when tainted is coming from url.URL.parse input'
122+
})
123+
124+
testThatRequestHasVulnerability(
125+
{
126+
fn: (req) => {
127+
const { URL } = require('url')
128+
const url = URL.parse('/path', req.body.url)
129+
const childProcess = require('child_process')
130+
childProcess.exec(url.host, noop)
131+
},
132+
vulnerability: 'COMMAND_INJECTION',
133+
occurrences: 1,
134+
cb: noop,
135+
makeRequest: makePostRequest,
136+
testDescription: 'should detect vulnerability when tainted is coming from url.URL.parse base'
137+
})
138+
}
139+
})
140+
141+
describe.skip('tainted path parameters', () => {
142+
function makeRequestWithPathParam (done) {
143+
// Note: This would require setting up a parameterized route in Fastify
144+
// For now, using query params as a substitute since the test setup is generic
145+
axios.get(`http://localhost:${config.port}/?id=malicious-path`).catch(done)
146+
}
147+
148+
testThatRequestHasVulnerability((req) => {
149+
const childProcess = require('child_process')
150+
// Simulating path parameter access through query for test purposes
151+
childProcess.exec(req.query.id, noop)
152+
}, 'COMMAND_INJECTION', 1, noop, makeRequestWithPathParam)
153+
})
154+
155+
// NO
156+
// describe.skip('tainted cookies', () => {
157+
// function makeRequestWithCookie (done) {
158+
// axios.get(`http://localhost:${config.port}/`, {
159+
// headers: {
160+
// Cookie: 'command=echo cookie-injection'
161+
// }
162+
// }).catch(done)
163+
// }
164+
165+
// testThatRequestHasVulnerability((req) => {
166+
// const childProcess = require('child_process')
167+
// // Access cookie through raw request since Fastify cookie parsing may vary
168+
// const cookieHeader = req.headers.cookie
169+
// if (cookieHeader) {
170+
// const command = cookieHeader.split('=')[1]
171+
// childProcess.exec(command, noop)
172+
// }
173+
// }, 'COMMAND_INJECTION', 1, noop, makeRequestWithCookie)
174+
// })
175+
}
176+
)
177+
})
178+
})

0 commit comments

Comments
 (0)