Skip to content

Commit eec4d28

Browse files
authored
ESM support for iast (#5012)
1 parent c7b0c18 commit eec4d28

File tree

14 files changed

+550
-16
lines changed

14 files changed

+550
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict'
2+
3+
function dummyOperation (a) {
4+
return a + 'should have ' + 'dummy operation to be rewritten' + ' without crashing'
5+
}
6+
7+
export async function initialize () {
8+
dummyOperation('should have')
9+
}
10+
11+
export async function load (url, context, nextLoad) {
12+
return nextLoad(url, context)
13+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict'
2+
3+
import childProcess from 'node:child_process'
4+
import express from 'express'
5+
import Module from 'node:module'
6+
import './worker.mjs'
7+
8+
const app = express()
9+
const port = process.env.APP_PORT || 3000
10+
11+
app.get('/cmdi-vulnerable', (req, res) => {
12+
childProcess.execSync(`ls ${req.query.args}`)
13+
14+
res.end()
15+
})
16+
17+
app.use('/more', (await import('./more.mjs')).default)
18+
19+
app.listen(port, () => {
20+
process.send({ port })
21+
})
22+
23+
Module.register('./custom-noop-hooks.mjs', {
24+
parentURL: import.meta.url
25+
})
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import express from 'express'
2+
import childProcess from 'node:child_process'
3+
4+
const router = express.Router()
5+
router.get('/cmdi-vulnerable', (req, res) => {
6+
childProcess.execSync(`ls ${req.query.args}`)
7+
8+
res.end()
9+
})
10+
11+
export default router
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict'
2+
3+
function dummyOperation (a) {
4+
return a + 'dummy operation with concat in worker-dep'
5+
}
6+
7+
dummyOperation('should not crash')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Worker, isMainThread } from 'node:worker_threads'
2+
import { URL } from 'node:url'
3+
import './worker-dep.mjs'
4+
5+
if (isMainThread) {
6+
const worker = new Worker(new URL(import.meta.url))
7+
worker.on('error', (e) => {
8+
throw e
9+
})
10+
} else {
11+
function dummyOperation (a) {
12+
return a + 'dummy operation with concat'
13+
}
14+
15+
dummyOperation('should not crash')
16+
}
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict'
2+
3+
const { createSandbox, spawnProc, FakeAgent } = require('../helpers')
4+
const path = require('path')
5+
const getPort = require('get-port')
6+
const Axios = require('axios')
7+
const { assert } = require('chai')
8+
9+
describe('ESM', () => {
10+
let axios, sandbox, cwd, appPort, appFile, agent, proc
11+
12+
before(async function () {
13+
this.timeout(process.platform === 'win32' ? 90000 : 30000)
14+
sandbox = await createSandbox(['express'])
15+
appPort = await getPort()
16+
cwd = sandbox.folder
17+
appFile = path.join(cwd, 'appsec', 'esm-app', 'index.mjs')
18+
19+
axios = Axios.create({
20+
baseURL: `http://localhost:${appPort}`
21+
})
22+
})
23+
24+
after(async function () {
25+
await sandbox.remove()
26+
})
27+
28+
const nodeOptionsList = [
29+
'--import dd-trace/initialize.mjs',
30+
'--require dd-trace/init.js --loader dd-trace/loader-hook.mjs'
31+
]
32+
33+
nodeOptionsList.forEach(nodeOptions => {
34+
describe(`with NODE_OPTIONS=${nodeOptions}`, () => {
35+
beforeEach(async () => {
36+
agent = await new FakeAgent().start()
37+
38+
proc = await spawnProc(appFile, {
39+
cwd,
40+
env: {
41+
DD_TRACE_AGENT_PORT: agent.port,
42+
APP_PORT: appPort,
43+
DD_IAST_ENABLED: 'true',
44+
DD_IAST_REQUEST_SAMPLING: '100',
45+
NODE_OPTIONS: nodeOptions
46+
}
47+
})
48+
})
49+
50+
afterEach(async () => {
51+
proc.kill()
52+
await agent.stop()
53+
})
54+
55+
function verifySpan (payload, verify) {
56+
let err
57+
for (let i = 0; i < payload.length; i++) {
58+
const trace = payload[i]
59+
for (let j = 0; j < trace.length; j++) {
60+
try {
61+
verify(trace[j])
62+
return
63+
} catch (e) {
64+
err = err || e
65+
}
66+
}
67+
}
68+
throw err
69+
}
70+
71+
it('should detect COMMAND_INJECTION vulnerability', async function () {
72+
await axios.get('/cmdi-vulnerable?args=-la')
73+
74+
await agent.assertMessageReceived(({ payload }) => {
75+
verifySpan(payload, span => {
76+
assert.property(span.meta, '_dd.iast.json')
77+
assert.include(span.meta['_dd.iast.json'], '"COMMAND_INJECTION"')
78+
})
79+
}, null, 1, true)
80+
})
81+
82+
it('should detect COMMAND_INJECTION vulnerability in imported file', async () => {
83+
await axios.get('/more/cmdi-vulnerable?args=-la')
84+
85+
await agent.assertMessageReceived(({ payload }) => {
86+
verifySpan(payload, span => {
87+
assert.property(span.meta, '_dd.iast.json')
88+
assert.include(span.meta['_dd.iast.json'], '"COMMAND_INJECTION"')
89+
})
90+
}, null, 1, true)
91+
})
92+
})
93+
})
94+
})

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
"dependencies": {
8484
"@datadog/libdatadog": "^0.4.0",
8585
"@datadog/native-appsec": "8.4.0",
86-
"@datadog/native-iast-rewriter": "2.6.1",
86+
"@datadog/native-iast-rewriter": "2.8.0",
8787
"@datadog/native-iast-taint-tracking": "3.2.0",
8888
"@datadog/native-metrics": "^3.1.0",
8989
"@datadog/pprof": "5.5.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict'
2+
3+
module.exports = {
4+
LOG_MESSAGE: 'LOG',
5+
REWRITTEN_MESSAGE: 'REWRITTEN'
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict'
2+
3+
import path from 'path'
4+
import { URL } from 'url'
5+
import { getName } from '../telemetry/verbosity.js'
6+
import { isNotLibraryFile, isPrivateModule } from './filter.js'
7+
import constants from './constants.js'
8+
9+
const currentUrl = new URL(import.meta.url)
10+
const ddTraceDir = path.join(currentUrl.pathname, '..', '..', '..', '..', '..', '..')
11+
12+
let port, rewriter
13+
14+
export async function initialize (data) {
15+
if (rewriter) return Promise.reject(new Error('ALREADY INITIALIZED'))
16+
17+
const { csiMethods, telemetryVerbosity, chainSourceMap } = data
18+
port = data.port
19+
20+
const iastRewriter = await import('@datadog/native-iast-rewriter')
21+
22+
const { NonCacheRewriter } = iastRewriter.default
23+
24+
rewriter = new NonCacheRewriter({
25+
csiMethods,
26+
telemetryVerbosity: getName(telemetryVerbosity),
27+
chainSourceMap
28+
})
29+
}
30+
31+
export async function load (url, context, nextLoad) {
32+
const result = await nextLoad(url, context)
33+
34+
if (!port) return result
35+
if (!result.source) return result
36+
if (url.includes(ddTraceDir) || url.includes('iitm=true')) return result
37+
38+
try {
39+
if (isPrivateModule(url) && isNotLibraryFile(url)) {
40+
const rewritten = rewriter.rewrite(result.source.toString(), url)
41+
42+
if (rewritten?.content) {
43+
result.source = rewritten.content || result.source
44+
const data = { url, rewritten }
45+
port.postMessage({ type: constants.REWRITTEN_MESSAGE, data })
46+
}
47+
}
48+
} catch (e) {
49+
const newErrObject = {
50+
message: e.message,
51+
stack: e.stack
52+
}
53+
54+
const data = {
55+
level: 'error',
56+
messages: ['[ASM] Error rewriting file %s', url, newErrObject]
57+
}
58+
port.postMessage({
59+
type: constants.LOG_MESSAGE,
60+
data
61+
})
62+
}
63+
64+
return result
65+
}

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

+14-5
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ const telemetryRewriter = {
1212
information (content, filename, rewriter) {
1313
const response = this.off(content, filename, rewriter)
1414

15-
const metrics = response.metrics
16-
if (metrics && metrics.instrumentedPropagation) {
17-
INSTRUMENTED_PROPAGATION.inc(undefined, metrics.instrumentedPropagation)
18-
}
15+
incrementTelemetry(response.metrics)
1916

2017
return response
2118
}
@@ -30,4 +27,16 @@ function getRewriteFunction (rewriter) {
3027
}
3128
}
3229

33-
module.exports = { getRewriteFunction }
30+
function incrementTelemetry (metrics) {
31+
if (metrics?.instrumentedPropagation) {
32+
INSTRUMENTED_PROPAGATION.inc(undefined, metrics.instrumentedPropagation)
33+
}
34+
}
35+
36+
function incrementTelemetryIfNeeded (metrics) {
37+
if (iastTelemetry.verbosity !== Verbosity.OFF) {
38+
incrementTelemetry(metrics)
39+
}
40+
}
41+
42+
module.exports = { getRewriteFunction, incrementTelemetryIfNeeded }

0 commit comments

Comments
 (0)