Skip to content

Commit ba308d4

Browse files
authored
fix(iast): Fix stack traces when iast is enabled and application has --enable-source-maps (#6828)
1 parent ebd99f4 commit ba308d4

File tree

9 files changed

+201
-11
lines changed

9 files changed

+201
-11
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import express from 'express'
2+
3+
import notRewrittenRoutes from './not-rewritten-routes'
4+
import rewrittenRoutes from './rewritten-routes'
5+
6+
const app = express()
7+
8+
app.use('/not-rewritten', notRewrittenRoutes)
9+
app.use('/rewritten', rewrittenRoutes)
10+
11+
const server = app.listen(process.env.APP_PORT || 0, () => {
12+
process.send?.({ port: server.address().port })
13+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict'
2+
const tracer = require('dd-trace')
3+
4+
tracer.init({
5+
flushInterval: 0
6+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import express from 'express'
2+
import crypto from 'crypto'
3+
4+
const router = express.Router()
5+
6+
router.get('/stack-trace-from-unnamed-function', (req, res) => {
7+
res.send((new Error('Error').stack))
8+
})
9+
10+
router.get('/stack-trace-from-named-function', function namedFunctionNotRewrittenFile (req, res) {
11+
res.send((new Error('Error').stack))
12+
})
13+
14+
router.get('/vulnerability', (req, res) => {
15+
res.send(crypto.createHash('sha1').update('test').digest('hex'))
16+
})
17+
18+
export default router
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import express from 'express'
2+
import crypto from 'crypto'
3+
4+
const router = express.Router()
5+
6+
router.get('/stack-trace-from-unnamed-function', (req, res) => {
7+
res.send((new Error('Error ' + req.query.param).stack))
8+
})
9+
10+
router.get('/stack-trace-from-named-function', function namedFunctionRewrittenFile (req, res) {
11+
res.send((new Error('Error ' + req.query.param).stack))
12+
})
13+
14+
router.get('/vulnerability', (req, res) => {
15+
res.send(crypto.createHash('sha1').update('test').digest('hex'))
16+
})
17+
18+
export default router
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "commonjs",
5+
"lib": ["ES2020"],
6+
"outDir": "./",
7+
"rootDir": "./",
8+
"sourceMap": true,
9+
"declaration": false,
10+
"strict": false,
11+
"esModuleInterop": true,
12+
"skipLibCheck": true,
13+
"forceConsistentCasingInFileNames": true,
14+
"moduleResolution": "node",
15+
"resolveJsonModule": true
16+
},
17+
"include": [
18+
"*.ts"
19+
],
20+
"exclude": [
21+
"node_modules"
22+
]
23+
}
24+
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict'
2+
3+
const { sandboxCwd, useSandbox, spawnProc, FakeAgent } = require('../helpers')
4+
const childProcess = require('child_process')
5+
const path = require('path')
6+
const Axios = require('axios')
7+
const { assert } = require('chai')
8+
9+
describe('IAST stack traces and vulnerabilities with sourcemaps', () => {
10+
let axios, cwd, appDir, appFile, agent, proc
11+
12+
useSandbox(['typescript', 'express'])
13+
14+
before(function () {
15+
cwd = sandboxCwd()
16+
17+
appDir = path.join(cwd, 'appsec', 'iast-stack-traces-ts-with-sourcemaps')
18+
19+
childProcess.execSync('yarn', { cwd })
20+
childProcess.execSync('npx tsc', {
21+
cwd: appDir
22+
})
23+
24+
appFile = path.join(appDir, 'index.js')
25+
})
26+
27+
beforeEach(async () => {
28+
agent = await new FakeAgent().start()
29+
30+
proc = await spawnProc(appFile, {
31+
cwd,
32+
env: {
33+
DD_TRACE_AGENT_PORT: agent.port,
34+
DD_IAST_ENABLED: 'true',
35+
DD_IAST_REQUEST_SAMPLING: '100',
36+
NODE_OPTIONS: `--enable-source-maps --require ${appDir}/init.js`
37+
}
38+
})
39+
40+
axios = Axios.create({ baseURL: proc.url })
41+
})
42+
43+
afterEach(async () => {
44+
proc.kill()
45+
await agent.stop()
46+
})
47+
48+
describe('in rewritten file', () => {
49+
it('should detect correct stack trace in unnamed function', async () => {
50+
const response = await axios.get('/rewritten/stack-trace-from-unnamed-function')
51+
52+
assert.include(response.data, '/rewritten-routes.ts:7:13')
53+
})
54+
55+
it('should detect correct stack trace in named function', async () => {
56+
const response = await axios.get('/rewritten/stack-trace-from-named-function')
57+
58+
assert.include(response.data, '/rewritten-routes.ts:11:13')
59+
})
60+
61+
it('should detect vulnerability in the correct location', async () => {
62+
await axios.get('/rewritten/vulnerability')
63+
64+
await agent.assertMessageReceived(({ payload }) => {
65+
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request'))
66+
spans.forEach(span => {
67+
assert.property(span.meta, '_dd.iast.json')
68+
const iastJsonObject = JSON.parse(span.meta['_dd.iast.json'])
69+
70+
assert.isTrue(iastJsonObject.vulnerabilities.some(vulnerability => {
71+
return vulnerability.type === 'WEAK_HASH' &&
72+
vulnerability.location.path === 'appsec/iast-stack-traces-ts-with-sourcemaps/rewritten-routes.ts' &&
73+
vulnerability.location.line === 15
74+
}))
75+
})
76+
}, null, 1, true)
77+
})
78+
})
79+
80+
describe('in not rewritten file', () => {
81+
it('should detect correct stack trace in unnamed function', async () => {
82+
const response = await axios.get('/not-rewritten/stack-trace-from-unnamed-function')
83+
84+
assert.include(response.data, '/not-rewritten-routes.ts:7:13')
85+
})
86+
87+
it('should detect correct stack trace in named function', async () => {
88+
const response = await axios.get('/not-rewritten/stack-trace-from-named-function')
89+
90+
assert.include(response.data, '/not-rewritten-routes.ts:11:13')
91+
})
92+
93+
it('should detect vulnerability in the correct location', async () => {
94+
await axios.get('/not-rewritten/vulnerability')
95+
96+
await agent.assertMessageReceived(({ payload }) => {
97+
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request'))
98+
spans.forEach(span => {
99+
assert.property(span.meta, '_dd.iast.json')
100+
const iastJsonObject = JSON.parse(span.meta['_dd.iast.json'])
101+
102+
assert.isTrue(iastJsonObject.vulnerabilities.some(vulnerability => {
103+
return vulnerability.type === 'WEAK_HASH' &&
104+
vulnerability.location.path === 'appsec/iast-stack-traces-ts-with-sourcemaps/not-rewritten-routes.ts' &&
105+
vulnerability.location.line === 15
106+
}))
107+
})
108+
}, null, 1, true)
109+
})
110+
})
111+
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
"@datadog/openfeature-node-server": "0.1.0-preview.15",
129129
"@datadog/pprof": "5.12.0",
130130
"@datadog/sketches-js": "2.1.1",
131-
"@datadog/wasm-js-rewriter": "4.0.1",
131+
"@datadog/wasm-js-rewriter": "5.0.1",
132132
"@isaacs/ttlcache": "^2.0.1",
133133
"@opentelemetry/api": ">=1.0.0 <1.10.0",
134134
"@opentelemetry/api-logs": "<1.0.0",

packages/dd-trace/src/appsec/stack_trace.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ function filterOutFramesFromLibrary (callSiteList) {
4242
if (globalThis.__DD_ESBUILD_IAST_WITH_SM) {
4343
// bundled with SourceMap, get original file and line to discriminate if comes from dd-trace or not
4444
const callSiteLocation = {
45-
path: callSite.getFileName(),
46-
line: callSite.getLineNumber(),
47-
column: callSite.getColumnNumber()
45+
path: callSite.getTranslatedFileName?.() ?? callSite.getFileName(),
46+
line: callSite.getTranslatedLineNumber?.() ?? callSite.getLineNumber(),
47+
column: callSite.getTranslatedColumnNumber?.() ?? callSite.getColumnNumber()
4848
}
4949
const { path } = getOriginalPathAndLineFromSourceMap(callSiteLocation)
5050
return !path?.startsWith(ddBasePath)
@@ -68,9 +68,9 @@ function getCallsiteFrames (maxDepth = 32, constructorOpt = getCallsiteFrames, c
6868
const callSite = filteredFrames[index]
6969
indexedFrames.push({
7070
id: index,
71-
file: callSite.getFileName(),
72-
line: callSite.getLineNumber(),
73-
column: callSite.getColumnNumber(),
71+
file: callSite.getTranslatedFileName?.() ?? callSite.getFileName(),
72+
line: callSite.getTranslatedLineNumber?.() ?? callSite.getLineNumber(),
73+
column: callSite.getTranslatedColumnNumber?.() ?? callSite.getColumnNumber(),
7474
function: callSite.getFunctionName(),
7575
class_name: callSite.getTypeName(),
7676
isNative: callSite.isNative()

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,10 @@
262262
resolved "https://registry.yarnpkg.com/@datadog/sketches-js/-/sketches-js-2.1.1.tgz#9ec2251b3c932b4f43e1d164461fa6cb6f28b7d0"
263263
integrity sha512-d5RjycE+MObE/hU+8OM5Zp4VjTwiPLRa8299fj7muOmR16fb942z8byoMbCErnGh0lBevvgkGrLclQDvINbIyg==
264264

265-
"@datadog/wasm-js-rewriter@4.0.1":
266-
version "4.0.1"
267-
resolved "https://registry.yarnpkg.com/@datadog/wasm-js-rewriter/-/wasm-js-rewriter-4.0.1.tgz#883535e97f6b88b15427f93b5dc5d2d3a01c02b6"
268-
integrity sha512-JRa05Je6gw+9+3yZnm/BroQZrEfNwRYCxms56WCCHzOBnoPihQLB0fWy5coVJS29kneCUueUvBvxGp6NVXgdqw==
265+
"@datadog/wasm-js-rewriter@5.0.1":
266+
version "5.0.1"
267+
resolved "https://registry.yarnpkg.com/@datadog/wasm-js-rewriter/-/wasm-js-rewriter-5.0.1.tgz#f227d2e3eb0f83b8a37f190a9ff8fdbde5955782"
268+
integrity sha512-EzbV3Lrdt3udQEsbDOVC5gB1y7yxfpBbrSIk4jaEsGjyj0Dbv2HGj7tZjs+qXzIzNonHc8h5El2bYZOGfC2wwg==
269269
dependencies:
270270
js-yaml "^4.1.0"
271271
lru-cache "^7.14.0"

0 commit comments

Comments
 (0)