Skip to content

Commit 7f1e8fd

Browse files
authored
fix: receiving central config with sanitize_field_names would crash (#3249)
The crash was with init of config.sanitizeFieldNamesRegExp in the central-config code path. I've fixed that. Other updates: - Updated config normalization of this var -- and similar config vars that set a `${name}RegExp` array var -- initialize the RegExp array to empty before populating it. - Updated the tests to ensure we always add to "central-config-enabled.test.js" whenever a new supported central config var is added. - Updated central-config handling to try/catch and log.error instead of *crashing* if there is an unexpected exception. Partially applied central config isn't great, but it is better than having a DoS avenue where central config can crash agents. For example: {"log.level":"error",...,"remoteConf":{"sanitize_field_names":"password, pass*"},"error":{"type":"TypeError","message":"Cannot read properties of undefined (reading 'push')","stack_trace":"..."},"message":"Central config error: exception while applying changes"} Fixes: #3247
1 parent 42d7e69 commit 7f1e8fd

File tree

3 files changed

+85
-78
lines changed

3 files changed

+85
-78
lines changed

CHANGELOG.asciidoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ Notes:
3232
=== Node.js Agent version 3.x
3333
3434
35+
==== Unreleased
36+
37+
[float]
38+
===== Breaking changes
39+
40+
[float]
41+
===== Features
42+
43+
[float]
44+
===== Bug fixes
45+
46+
* Fix an issue where the APM agent receiving central config (from APM server)
47+
containing a value for `sanitized_field_names` would crash.
48+
({issues}3247[#3247])
49+
50+
[float]
51+
===== Chores
52+
53+
3554
[[release-notes-3.44.0]]
3655
==== 3.44.0 2023/04/03
3756

lib/config.js

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ var ENV_TABLE = {
185185
verifyServerCert: 'ELASTIC_APM_VERIFY_SERVER_CERT'
186186
}
187187

188-
var CENTRAL_CONFIG = {
188+
var CENTRAL_CONFIG_OPTS = {
189189
log_level: 'logLevel',
190190
transaction_sample_rate: 'transactionSampleRate',
191191
transaction_max_spans: 'transactionMaxSpans',
@@ -478,34 +478,36 @@ class Config {
478478

479479
transport.on('config', remoteConf => {
480480
agent.logger.debug({ remoteConf }, 'central config received')
481-
const conf = {}
482-
const unknown = []
483-
484-
for (const [key, value] of Object.entries(remoteConf)) {
485-
const newKey = CENTRAL_CONFIG[key]
486-
if (newKey) {
487-
conf[newKey] = value
488-
} else {
489-
unknown.push(key)
481+
try {
482+
const conf = {}
483+
const unknown = []
484+
485+
for (const [key, value] of Object.entries(remoteConf)) {
486+
const newKey = CENTRAL_CONFIG_OPTS[key]
487+
if (newKey) {
488+
conf[newKey] = value
489+
} else {
490+
unknown.push(key)
491+
}
492+
}
493+
if (unknown.length > 0) {
494+
agent.logger.warn(`Central config warning: unsupported config names: ${unknown.join(', ')}`)
490495
}
491-
}
492-
493-
if (unknown.length > 0) {
494-
agent.logger.warn(`Central config warning: unsupported config names: ${unknown.join(', ')}`)
495-
}
496-
497-
if (Object.keys(conf).length > 0) {
498-
normalize(conf, agent.logger)
499496

500-
for (const [key, value] of Object.entries(conf)) {
501-
const oldValue = agent._conf[key]
502-
agent._conf[key] = value
503-
if (key === 'logLevel' && value !== oldValue && !logging.isLoggerCustom(agent.logger)) {
504-
logging.setLogLevel(agent.logger, value)
505-
agent.logger.info(`Central config success: updated logger with new logLevel: ${value}`)
497+
if (Object.keys(conf).length > 0) {
498+
normalize(conf, agent.logger)
499+
for (const [key, value] of Object.entries(conf)) {
500+
const oldValue = agent._conf[key]
501+
agent._conf[key] = value
502+
if (key === 'logLevel' && value !== oldValue && !logging.isLoggerCustom(agent.logger)) {
503+
logging.setLogLevel(agent.logger, value)
504+
agent.logger.info(`Central config success: updated logger with new logLevel: ${value}`)
505+
}
506+
agent.logger.info(`Central config success: updated ${key}: ${value}`)
506507
}
507-
agent.logger.info(`Central config success: updated ${key}: ${value}`)
508508
}
509+
} catch (err) {
510+
agent.logger.error({ remoteConf, err }, 'Central config error: exception while applying changes')
509511
}
510512
})
511513

@@ -896,6 +898,7 @@ function normalizeTransactionSampleRate (opts, logger) {
896898

897899
function normalizeSanitizeFieldNames (opts) {
898900
if (opts.sanitizeFieldNames) {
901+
opts.sanitizeFieldNamesRegExp = []
899902
const wildcard = new WildcardMatcher()
900903
for (const ptn of opts.sanitizeFieldNames) {
901904
const re = wildcard.compile(ptn)
@@ -917,12 +920,7 @@ function normalizeCloudProvider (opts, logger) {
917920

918921
function normalizeIgnoreOptions (opts) {
919922
if (opts.transactionIgnoreUrls) {
920-
// We can't guarantee that opts will be a Config so set a
921-
// default value. This is to work around CENTRAL_CONFIG tests
922-
// that call this method with a plain object `{}`
923-
if (!opts.transactionIgnoreUrlRegExp) {
924-
opts.transactionIgnoreUrlRegExp = []
925-
}
923+
opts.transactionIgnoreUrlRegExp = []
926924
const wildcard = new WildcardMatcher()
927925
for (const ptn of opts.transactionIgnoreUrls) {
928926
const re = wildcard.compile(ptn)
@@ -931,25 +929,33 @@ function normalizeIgnoreOptions (opts) {
931929
}
932930

933931
if (opts.ignoreUrls) {
932+
opts.ignoreUrlStr = []
933+
opts.ignoreUrlRegExp = []
934934
for (const ptn of opts.ignoreUrls) {
935-
if (typeof ptn === 'string') opts.ignoreUrlStr.push(ptn)
936-
else opts.ignoreUrlRegExp.push(ptn)
935+
if (typeof ptn === 'string') {
936+
opts.ignoreUrlStr.push(ptn)
937+
} else {
938+
opts.ignoreUrlRegExp.push(ptn)
939+
}
937940
}
938941
delete opts.ignoreUrls
939942
}
940943

941944
if (opts.ignoreUserAgents) {
945+
opts.ignoreUserAgentStr = []
946+
opts.ignoreUserAgentRegExp = []
942947
for (const ptn of opts.ignoreUserAgents) {
943-
if (typeof ptn === 'string') opts.ignoreUserAgentStr.push(ptn)
944-
else opts.ignoreUserAgentRegExp.push(ptn)
948+
if (typeof ptn === 'string') {
949+
opts.ignoreUserAgentStr.push(ptn)
950+
} else {
951+
opts.ignoreUserAgentRegExp.push(ptn)
952+
}
945953
}
946954
delete opts.ignoreUserAgents
947955
}
948956

949957
if (opts.ignoreMessageQueues) {
950-
if (!opts.ignoreMessageQueuesRegExp) {
951-
opts.ignoreMessageQueuesRegExp = []
952-
}
958+
opts.ignoreMessageQueuesRegExp = []
953959
const wildcard = new WildcardMatcher()
954960
for (const ptn of opts.ignoreMessageQueues) {
955961
const re = wildcard.compile(ptn)
@@ -960,6 +966,7 @@ function normalizeIgnoreOptions (opts) {
960966

961967
function normalizeElasticsearchCaptureBodyUrls (opts) {
962968
if (opts.elasticsearchCaptureBodyUrls) {
969+
opts.elasticsearchCaptureBodyUrlsRegExp = []
963970
const wildcard = new WildcardMatcher()
964971
for (const ptn of opts.elasticsearchCaptureBodyUrls) {
965972
const re = wildcard.compile(ptn)
@@ -1354,6 +1361,7 @@ module.exports = {
13541361
TRACE_CONTINUATION_STRATEGY_RESTART_EXTERNAL,
13551362

13561363
// The following are exported for tests.
1364+
CENTRAL_CONFIG_OPTS,
13571365
DEFAULTS,
13581366
DURATION_OPTS,
13591367
secondsFromDuration,

test/central-config-enabled.test.js

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ const { URL } = require('url')
1313
const http = require('http')
1414

1515
const test = require('tape')
16+
1617
const Agent = require('./_agent')
18+
const { CENTRAL_CONFIG_OPTS } = require('../lib/config')
1719

1820
const runTestsWithServer = (t, updates, expect) => {
1921
let agent
@@ -65,59 +67,37 @@ const runTestsWithServer = (t, updates, expect) => {
6567

6668
test('remote config enabled', function (t) {
6769
const updates = {
68-
transaction_sample_rate: '0.42',
69-
transaction_max_spans: '99',
7070
capture_body: 'all',
71-
transaction_ignore_urls: ['foo'],
71+
exit_span_min_duration: '25ms',
72+
ignore_message_queues: 'spam,eggs, (?-i)ham ',
7273
log_level: 'warn',
74+
sanitize_field_names: 'password, pass*, *auth*',
7375
span_stack_trace_min_duration: '50ms',
7476
trace_continuation_strategy: 'restart_external',
75-
exit_span_min_duration: '25ms'
77+
// Test that central config returing an array works, even though, IIUC,
78+
// it will always return plain strings.
79+
transaction_ignore_urls: ['foo', 'bar'],
80+
transaction_max_spans: '99',
81+
transaction_sample_rate: '0.42'
7682
}
7783
const expect = {
78-
transactionSampleRate: 0.42,
79-
transactionMaxSpans: 99,
8084
captureBody: 'all',
81-
transactionIgnoreUrls: ['foo'],
85+
exitSpanMinDuration: 0.025,
86+
ignoreMessageQueues: ['spam', 'eggs', '(?-i)ham'],
87+
ignoreMessageQueuesRegExp: [/^spam$/i, /^eggs$/i, /^ham$/],
8288
logLevel: 'warn',
89+
sanitizeFieldNames: ['password', 'pass*', '*auth*'],
90+
sanitizeFieldNamesRegExp: [/^password$/i, /^pass.*$/i, /^.*auth.*$/i],
8391
spanStackTraceMinDuration: 0.05,
8492
traceContinuationStrategy: 'restart_external',
85-
exitSpanMinDuration: 0.025
86-
}
87-
88-
runTestsWithServer(t, updates, expect)
89-
})
90-
91-
test('remote config enabled: receives comma delimited', function (t) {
92-
const updates = {
93-
transaction_sample_rate: '0.42',
94-
transaction_max_spans: '99',
95-
capture_body: 'all',
96-
transaction_ignore_urls: 'foo,bar , baz , bling'
97-
}
98-
const expect = {
99-
transactionSampleRate: 0.42,
93+
transactionIgnoreUrls: ['foo', 'bar'],
94+
transactionIgnoreUrlRegExp: [/^foo$/i, /^bar$/i],
10095
transactionMaxSpans: 99,
101-
captureBody: 'all',
102-
transactionIgnoreUrls: ['foo', 'bar', 'baz', 'bling']
96+
transactionSampleRate: 0.42
10397
}
10498

105-
runTestsWithServer(t, updates, expect)
106-
})
107-
108-
test('remote config enabled: receives non delimited string', function (t) {
109-
const updates = {
110-
transaction_sample_rate: '0.42',
111-
transaction_max_spans: '99',
112-
capture_body: 'all',
113-
transaction_ignore_urls: 'foo:bar'
114-
}
115-
const expect = {
116-
transactionSampleRate: 0.42,
117-
transactionMaxSpans: 99,
118-
captureBody: 'all',
119-
transactionIgnoreUrls: ['foo:bar']
120-
}
99+
t.deepEqual(Object.keys(updates).sort(), Object.keys(CENTRAL_CONFIG_OPTS).sort(),
100+
'this test uses every available central config var name (config.CENTRAL_CONFIG_OPTS)')
121101

122102
runTestsWithServer(t, updates, expect)
123103
})

0 commit comments

Comments
 (0)