Skip to content

Commit 11f5fcc

Browse files
authored
fix: fix AppSec http memory retention (#8029)
* fix: fix AppSec http memory retention When AppSec is active, http calls have a long memory retention due to a strong reference behind held. That reference is freed at the end of a span for example, but it takes additional time and shows in many memory profiles. Fix that by using a weak reference and a specific handling to circumvent that retention issue. * fix: guarantee runtime metrics client is defined This is a sync call, so it has never be an issue. If however anything would report a metric sync from the native module at construction time, client would have been undefined. Fix that by moving the client construction before any possible usage.
1 parent 08322b5 commit 11f5fcc

26 files changed

Lines changed: 344 additions & 148 deletions

packages/datadog-instrumentations/test/passport-http.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const { after, before, beforeEach, describe, it } = require('mocha')
88
const sinon = require('sinon')
99

1010
const agent = require('../../dd-trace/test/plugins/agent')
11-
const { storage } = require('../../datadog-core')
11+
const { getActiveRequest } = require('../../dd-trace/src/appsec/store')
1212
const { withVersions } = require('../../dd-trace/test/setup/mocha')
1313

1414
withVersions('passport-http', 'passport-http', version => {
@@ -185,7 +185,8 @@ withVersions('passport-http', 'passport-http', version => {
185185

186186
it('should block when subscriber aborts', async () => {
187187
subscriberStub = sinon.spy(({ abortController }) => {
188-
storage('legacy').getStore().req.res.writeHead(403).end('Blocked')
188+
const req = getActiveRequest()
189+
req.res.writeHead(403).end('Blocked')
189190
abortController.abort()
190191
})
191192

packages/datadog-instrumentations/test/passport-local.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const sinon = require('sinon')
88

99
const axios = require('axios').create({ validateStatus: null })
1010
const agent = require('../../dd-trace/test/plugins/agent')
11-
const { storage } = require('../../datadog-core')
11+
const { getActiveRequest } = require('../../dd-trace/src/appsec/store')
1212
const { withVersions } = require('../../dd-trace/test/setup/mocha')
1313

1414
withVersions('passport-local', 'passport-local', version => {
@@ -164,7 +164,8 @@ withVersions('passport-local', 'passport-local', version => {
164164

165165
it('should block when subscriber aborts', async () => {
166166
subscriberStub = sinon.spy(({ abortController }) => {
167-
storage('legacy').getStore().req.res.writeHead(403).end('Blocked')
167+
const req = getActiveRequest()
168+
req.res.writeHead(403).end('Blocked')
168169
abortController.abort()
169170
})
170171

packages/datadog-instrumentations/test/passport.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const sinon = require('sinon')
77

88
const axios = require('axios').create({ validateStatus: null })
99
const agent = require('../../dd-trace/test/plugins/agent')
10-
const { storage } = require('../../datadog-core')
10+
const { getActiveRequest } = require('../../dd-trace/src/appsec/store')
1111
const { withVersions } = require('../../dd-trace/test/setup/mocha')
1212

1313
const users = [
@@ -149,7 +149,8 @@ withVersions('passport', 'passport', version => {
149149
const cookie = login.headers['set-cookie'][0]
150150

151151
subscriberStub.callsFake(({ abortController }) => {
152-
const res = storage('legacy').getStore().req.res
152+
const req = getActiveRequest()
153+
const res = req.res
153154
res.writeHead(403)
154155
res.constructor.prototype.end.call(res, 'Blocked')
155156
abortController.abort()

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
const ServerPlugin = require('../../dd-trace/src/plugins/server')
44
const { storage } = require('../../datadog-core')
5+
const { withRequest } = require('../../dd-trace/src/appsec/store')
56
const web = require('../../dd-trace/src/plugins/util/web')
67
const { incomingHttpRequestStart, incomingHttpRequestEnd } = require('../../dd-trace/src/appsec/channels')
78
const { COMPONENT, SVC_SRC_KEY } = require('../../dd-trace/src/constants')
89

10+
const legacyStorage = storage('legacy')
11+
912
class HttpServerPlugin extends ServerPlugin {
1013
static id = 'http'
1114

@@ -17,7 +20,7 @@ class HttpServerPlugin extends ServerPlugin {
1720
}
1821

1922
start ({ req, res, abortController }) {
20-
let store = storage('legacy').getStore()
23+
let store = legacyStorage.getStore()
2124
const { name: schemaServiceName, source: schemaServiceSource } = this.serviceName()
2225
const service = this.config.service || schemaServiceName
2326
const serviceSource = (this.config.service && service !== this.tracer._service)
@@ -40,15 +43,13 @@ class HttpServerPlugin extends ServerPlugin {
4043
span._integrationName = this.constructor.id
4144

4245
const context = web.getContext(req)
46+
context.parentStore = store
4347

44-
if (context) {
45-
context.parentStore = store
46-
}
47-
48-
// AppSec, IAST, and AI Guard need req/res on the store so downstream
49-
// subscribers can access them from the async context.
50-
if (incomingHttpRequestStart.hasSubscribers) {
51-
store = { ...store, req, res }
48+
const appsecActive = incomingHttpRequestStart.hasSubscribers
49+
if (appsecActive) {
50+
// AppSec, IAST, and AI Guard need req on the store so downstream
51+
// subscribers can access them from the async context.
52+
store = withRequest(store, req)
5253
}
5354

5455
this.enter(span, store)
@@ -58,7 +59,7 @@ class HttpServerPlugin extends ServerPlugin {
5859
context.instrumented = true
5960
}
6061

61-
if (incomingHttpRequestStart.hasSubscribers) {
62+
if (appsecActive) {
6263
incomingHttpRequestStart.publish({ req, res, abortController }) // TODO: no need to make a new object here
6364
}
6465
}

packages/datadog-plugin-http/test/server.spec.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const { afterEach, beforeEach, describe, it } = require('mocha')
77
const sinon = require('sinon')
88

99
const { incomingHttpRequestStart } = require('../../dd-trace/src/appsec/channels')
10+
const { storage } = require('../../datadog-core')
11+
const { getRequest } = require('../../dd-trace/src/appsec/store')
1012
const agent = require('../../dd-trace/test/plugins/agent')
1113
const { withNamingSchema } = require('../../dd-trace/test/setup/mocha')
1214
const { rawExpectedSchema } = require('./naming')
@@ -150,6 +152,28 @@ describe('Plugin', () => {
150152
axios.get(`http://localhost:${port}/user`).catch(done)
151153
})
152154

155+
it('should preserve request access through child scope activation without strong fields', done => {
156+
const spy = sinon.spy()
157+
incomingHttpRequestStart.subscribe(spy)
158+
159+
app = (req, res) => {
160+
const childSpan = tracer.startSpan('child')
161+
162+
tracer.scope().activate(childSpan, () => {
163+
const store = storage('legacy').getStore()
164+
165+
sinon.assert.calledOnce(spy)
166+
assert.strictEqual(getRequest(store), req)
167+
assert.strictEqual(Object.hasOwn(store, 'req'), false)
168+
assert.strictEqual(Object.hasOwn(store, 'res'), false)
169+
})
170+
171+
done()
172+
}
173+
174+
axios.get(`http://localhost:${port}/user`).catch(done)
175+
})
176+
153177
it('should run the request\'s close event in the correct context', done => {
154178
app = (req, res) => {
155179
req.on('close', () => {

packages/dd-trace/src/aiguard/sdk.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const rfdc = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false })
44
const { HTTP_CLIENT_IP, NETWORK_CLIENT_IP } = require('../../../../ext/tags')
5-
const { storage } = require('../../../datadog-core')
5+
const { getActiveRequest } = require('../appsec/store')
66
const log = require('../log')
77
const { extractIp } = require('../plugins/util/ip_extractor')
88
const telemetryMetrics = require('../telemetry/metrics')
@@ -154,7 +154,7 @@ class AIGuard extends NoopAIGuard {
154154

155155
if (!needsHttpClientIp && !needsNetworkClientIp) return
156156

157-
const req = storage('legacy').getStore()?.req
157+
const req = getActiveRequest()
158158

159159
if (!req) return
160160

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use strict'
22

3-
const { storage } = require('../../../datadog-core')
43
const log = require('../log')
54
const web = require('../plugins/util/web')
5+
const { getActiveRequest } = require('./store')
66
const {
77
addSpecificEndpoint,
88
specificBlockingTypes,
@@ -33,7 +33,7 @@ function disable () {
3333
}
3434

3535
function onGraphqlStartResolve ({ context, resolverInfo }) {
36-
const req = storage('legacy').getStore()?.req
36+
const req = getActiveRequest()
3737

3838
if (!req) return
3939

@@ -52,7 +52,7 @@ function onGraphqlStartResolve ({ context, resolverInfo }) {
5252
}
5353

5454
function enterInApolloMiddleware (data) {
55-
const req = data?.req || storage('legacy').getStore()?.req
55+
const req = data?.req || getActiveRequest()
5656
if (!req) return
5757

5858
graphqlRequestData.set(req, {
@@ -61,7 +61,7 @@ function enterInApolloMiddleware (data) {
6161
}
6262

6363
function enterInApolloServerCoreRequest () {
64-
const req = storage('legacy').getStore()?.req
64+
const req = getActiveRequest()
6565
if (!req) return
6666

6767
graphqlRequestData.set(req, {
@@ -71,7 +71,7 @@ function enterInApolloServerCoreRequest () {
7171
}
7272

7373
function enterInApolloRequest () {
74-
const req = storage('legacy').getStore()?.req
74+
const req = getActiveRequest()
7575

7676
const requestData = graphqlRequestData.get(req)
7777
if (requestData) {
@@ -83,7 +83,7 @@ function enterInApolloRequest () {
8383
}
8484

8585
function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) {
86-
const req = storage('legacy').getStore()?.req
86+
const req = getActiveRequest()
8787
if (!req) return
8888

8989
const requestData = graphqlRequestData.get(req)

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const log = require('../log')
44
const web = require('../plugins/util/web')
55
const { extractIp } = require('../plugins/util/ip_extractor')
66
const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
7-
const { storage } = require('../../../datadog-core')
87
const { IS_SERVERLESS } = require('../serverless')
98
const RuleManager = require('./rule_manager')
109
const appsecRemoteConfig = require('./remote_config')
@@ -40,6 +39,7 @@ const Reporter = require('./reporter')
4039
const appsecTelemetry = require('./telemetry')
4140
const apiSecuritySampler = require('./api_security_sampler')
4241
const { isBlocked, block, callBlockDelegation, setTemplates, getBlockingAction } = require('./blocking')
42+
const { getActiveRequest } = require('./store')
4343
const UserTracking = require('./user_tracking')
4444
const graphql = require('./graphql')
4545
const rasp = require('./rasp')
@@ -116,8 +116,7 @@ function onRequestBodyParsed ({ req, res, body, abortController }) {
116116
if (body === undefined || body === null) return
117117

118118
if (!req) {
119-
const store = storage('legacy').getStore()
120-
req = store?.req
119+
req = getActiveRequest()
121120
}
122121

123122
const rootSpan = web.root(req)
@@ -258,8 +257,8 @@ function incomingHttpEndTranslator ({ req, res }) {
258257
}
259258

260259
function onPassportVerify ({ framework, login, user, success, abortController }) {
261-
const store = storage('legacy').getStore()
262-
const rootSpan = store?.req && web.root(store.req)
260+
const req = getActiveRequest()
261+
const rootSpan = req && web.root(req)
263262

264263
if (!rootSpan) {
265264
log.warn('[ASM] No rootSpan found in onPassportVerify')
@@ -268,12 +267,12 @@ function onPassportVerify ({ framework, login, user, success, abortController })
268267

269268
const results = UserTracking.trackLogin(framework, login, user, success, rootSpan)
270269

271-
handleResults(results?.actions, store.req, store.req.res, rootSpan, abortController)
270+
handleResults(results?.actions, req, web.getContext(req)?.res, rootSpan, abortController)
272271
}
273272

274273
function onPassportDeserializeUser ({ user, abortController }) {
275-
const store = storage('legacy').getStore()
276-
const rootSpan = store?.req && web.root(store.req)
274+
const req = getActiveRequest()
275+
const rootSpan = req && web.root(req)
277276

278277
if (!rootSpan) {
279278
log.warn('[ASM] No rootSpan found in onPassportDeserializeUser')
@@ -282,7 +281,7 @@ function onPassportDeserializeUser ({ user, abortController }) {
282281

283282
const results = UserTracking.trackUser(user, rootSpan)
284283

285-
handleResults(results?.actions, store.req, store.req.res, rootSpan, abortController)
284+
handleResults(results?.actions, req, web.getContext(req)?.res, rootSpan, abortController)
286285
}
287286

288287
function onExpressSession ({ req, res, sessionId, abortController }) {
@@ -308,8 +307,7 @@ function onRequestQueryParsed ({ req, res, query, abortController }) {
308307
if (!query || typeof query !== 'object') return
309308

310309
if (!req) {
311-
const store = storage('legacy').getStore()
312-
req = store?.req
310+
req = getActiveRequest()
313311
}
314312

315313
const rootSpan = web.root(req)

packages/dd-trace/src/appsec/rasp/command_injection.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use strict'
22

33
const { childProcessExecutionTracingChannel } = require('../channels')
4-
const { storage } = require('../../../../datadog-core')
54
const addresses = require('../addresses')
5+
const web = require('../../plugins/util/web')
6+
const { getActiveRequest } = require('../store')
67
const waf = require('../waf')
78
const { RULE_TYPES, handleResult } = require('./utils')
89

@@ -27,8 +28,7 @@ function disable () {
2728
function analyzeCommandInjection ({ file, fileArgs, shell, abortController }) {
2829
if (!file) return
2930

30-
const store = storage('legacy').getStore()
31-
const req = store?.req
31+
const req = getActiveRequest()
3232
if (!req) return
3333

3434
const ephemeral = {}
@@ -46,8 +46,7 @@ function analyzeCommandInjection ({ file, fileArgs, shell, abortController }) {
4646

4747
const result = waf.run({ ephemeral }, req, raspRule)
4848

49-
const res = store?.res
50-
handleResult(result, req, res, abortController, config, raspRule)
49+
handleResult(result, req, web.getContext(req)?.res, abortController, config, raspRule)
5150
}
5251

5352
module.exports = {

packages/dd-trace/src/appsec/rasp/lfi.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ const { isAbsolute } = require('path')
44

55
const { fsOperationStart, incomingHttpRequestStart, expressResponseRenderStart } = require('../channels')
66
const { storage } = require('../../../../datadog-core')
7+
const web = require('../../plugins/util/web')
78
const { FS_OPERATION_PATH } = require('../addresses')
9+
const { getRequest } = require('../store')
810
const waf = require('../waf')
911
const { enable: enableFsPlugin, disable: disableFsPlugin, RASP_MODULE } = require('./fs-plugin')
1012
const { RULE_TYPES, handleResult } = require('./utils')
@@ -53,16 +55,18 @@ function analyzeLfiInResponseRender (ctx) {
5355
const store = storage('legacy').getStore()
5456
if (!store) return
5557

56-
analyzeLfiPath(ctx.view, ctx.req, store.res, ctx.abortController)
58+
analyzeLfiPath(ctx.view, ctx.req, web.getContext(ctx.req)?.res, ctx.abortController)
5759
}
5860

5961
function analyzeLfi (ctx) {
6062
const store = storage('legacy').getStore()
61-
if (!store) return
63+
const fs = store?.fs
64+
if (!fs) return
6265

63-
const { req, fs, res } = store
64-
if (!req || !fs) return
66+
const req = getRequest(store)
67+
if (!req) return
6568

69+
const res = web.getContext(req)?.res
6670
for (const path of getPaths(ctx, fs)) {
6771
analyzeLfiPath(path, req, res, ctx.abortController)
6872
}

0 commit comments

Comments
 (0)