Skip to content

Commit 89852de

Browse files
authored
fix: change how 'cookie' header is represented in trans to avoid possible mapping conflict (#4007)
Refs: #3322 Fixes: #4006
1 parent 041109a commit 89852de

File tree

6 files changed

+131
-15
lines changed

6 files changed

+131
-15
lines changed

CHANGELOG.asciidoc

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,47 @@ Notes:
3333
3434
See the <<upgrade-to-v4>> guide.
3535
36+
==== Unreleased
37+
38+
[float]
39+
===== Breaking changes
40+
41+
[float]
42+
===== Features
43+
44+
[float]
45+
===== Bug fixes
46+
47+
- Change how the "cookie" HTTP request header is represented in APM transaction
48+
data to avoid a rare, but possible, intake bug where the transaction could be
49+
rejected due to a mapping conflict.
50+
51+
Before this change a `Cookie: foo=bar; sessionid=42` HTTP request header
52+
would be represented in the transaction document in Elasticsearch with these
53+
document fields (the example assumes <<sanitize-field-names>> matches
54+
"sessionid", as it does by default):
55+
56+
```
57+
http.request.headers.cookie: "[REDACTED]"
58+
...
59+
http.request.cookies.foo: "bar"
60+
http.request.cookies.sessionid: "[REDACTED]"
61+
```
62+
63+
After this change it is represented as:
64+
65+
```
66+
http.request.headers.cookie: "foo=bar; sessionid=REDACTED"
67+
```
68+
69+
In other words, `http.request.cookies` are no longer separated out.
70+
({issues}4006[#4006])
71+
72+
73+
[float]
74+
===== Chores
75+
76+
3677
[[release-notes-4.5.3]]
3778
==== 4.5.3 - 2024/04/23
3879

lib/filters/sanitize-field-names.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,17 @@ function redactKeysFromPostedFormVariables(body, requestHeaders, regexes) {
4848
*
4949
* @param {Object} obj The source object be copied with redacted fields
5050
* @param {Array<RegExp>} regexes RegExps to check if the entry value needd to be redacted
51+
* @param {String} redactedStr The string to use for redacted values. Defaults to '[REDACTED]'.
5152
* @returns {Object} Copy of the source object with REDACTED entries or the original if falsy or regexes is not an array
5253
*/
53-
function redactKeysFromObject(obj, regexes) {
54+
function redactKeysFromObject(obj, regexes, redactedStr = REDACTED) {
5455
if (!obj || !Array.isArray(regexes)) {
5556
return obj;
5657
}
5758
const result = {};
5859
for (const key of Object.keys(obj)) {
5960
const shouldRedact = regexes.some((regex) => regex.test(key));
60-
result[key] = shouldRedact ? REDACTED : obj[key];
61+
result[key] = shouldRedact ? redactedStr : obj[key];
6162
}
6263
return result;
6364
}

lib/parsers.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ const {
2020
redactKeysFromPostedFormVariables,
2121
} = require('./filters/sanitize-field-names');
2222

23+
// When redacting individual cookie field values, this string is used instead
24+
// of `[REDACTED]`. The APM spec says:
25+
// > The replacement string SHOULD be `[REDACTED]`.
26+
// We diverge from spec here because, for better or worse, the `cookie` module
27+
// does `encodeURIComponent/decodeURIComponent` encoding on cookie fields. If we
28+
// used the brackets, then the reconstructed cookie would look like
29+
// `foo=bar; session-id=%5BREDACTED%5D`, which isn't helpful.
30+
const COOKIE_VAL_REDACTED = 'REDACTED';
31+
2332
/**
2433
* Extract appropriate `{transaction,error}.context.request` from an HTTP
2534
* request object. This handles header and body capture and redaction
@@ -61,14 +70,21 @@ function getContextFromRequest(req, conf, type) {
6170
conf.sanitizeFieldNamesRegExp,
6271
);
6372

64-
if (context.headers.cookie) {
65-
context.cookies = cookie.parse(req.headers.cookie);
66-
context.cookies = redactKeysFromObject(
67-
context.cookies,
73+
if (context.headers.cookie && context.headers.cookie !== REDACTED) {
74+
let cookies = cookie.parse(req.headers.cookie);
75+
cookies = redactKeysFromObject(
76+
cookies,
6877
conf.sanitizeFieldNamesRegExp,
78+
COOKIE_VAL_REDACTED,
6979
);
70-
// Redact the cookie to avoid data duplication
71-
context.headers.cookie = REDACTED;
80+
try {
81+
context.headers.cookie = Object.keys(cookies)
82+
.map((k) => cookie.serialize(k, cookies[k]))
83+
.join('; ');
84+
} catch (_err) {
85+
// Fallback to full redaction if there is an issue re-serializing.
86+
context.headers.cookie = REDACTED;
87+
}
7288
}
7389
}
7490

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"docs:open": "PREVIEW=1 npm run docs:build",
1010
"docs:build": "./docs/scripts/build_docs.sh apm-agent-nodejs ./docs ./build",
1111
"lint": "npm run lint:eslint && npm run lint:license-files && npm run lint:yaml-files && npm run lint:tav",
12-
"lint:eslint": "eslint # requires node >=18.18.0",
12+
"lint:eslint": "eslint . # requires node >=18.18.0",
1313
"lint:eslint-nostyle": "eslint --rule 'prettier/prettier: off' . # lint without checking style, not normally used; requires node>=18.18.0",
1414
"lint:fix": "eslint --fix . # requires node >=18.18.0",
1515
"lint:license-files": "./dev-utils/gen-notice.sh --lint . # requires node >=16",

test/instrumentation/transaction.test.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -553,15 +553,10 @@ test('#_encode() - http request meta data', function (t) {
553553
host: 'example.com',
554554
'user-agent': 'user-agent-header',
555555
'content-length': 42,
556-
cookie: '[REDACTED]',
556+
cookie: 'cookie1=foo; cookie2=bar; session-id=REDACTED',
557557
'x-foo': 'bar',
558558
'x-bar': 'baz',
559559
},
560-
cookies: {
561-
cookie1: 'foo',
562-
cookie2: 'bar',
563-
'session-id': '[REDACTED]',
564-
},
565560
body: '[REDACTED]',
566561
},
567562
});

test/parsers.test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var http = require('http');
1111
var test = require('tape');
1212

1313
var parsers = require('../lib/parsers');
14+
const { normalizeSanitizeFieldNames } = require('../lib/config/normalizers');
1415

1516
test('#getContextFromResponse()', function (t) {
1617
t.test('for error (before headers)', function (t) {
@@ -279,6 +280,68 @@ test('#getContextFromRequest()', function (t) {
279280
t.end();
280281
});
281282

283+
t.test('cookie header fields are sanitized', function (t) {
284+
const conf = { captureHeaders: true, sanitizeFieldNames: ['*session*'] };
285+
normalizeSanitizeFieldNames(conf);
286+
const req = {
287+
httpVersion: '1.1',
288+
method: 'GET',
289+
url: '/',
290+
headers: {
291+
host: 'example.com',
292+
cookie: 'foo=bar%3Bbaz; spam=eggs; sessionid=42',
293+
},
294+
};
295+
const parsed = parsers.getContextFromRequest(req, conf);
296+
t.deepEqual(parsed, {
297+
http_version: '1.1',
298+
method: 'GET',
299+
url: {
300+
raw: '/',
301+
protocol: 'http:',
302+
hostname: 'example.com',
303+
pathname: '/',
304+
full: 'http://example.com/',
305+
},
306+
headers: {
307+
host: 'example.com',
308+
cookie: 'foo=bar%3Bbaz; spam=eggs; sessionid=REDACTED',
309+
},
310+
});
311+
t.end();
312+
});
313+
314+
t.test('cookie header is in sanitizeFieldNames', function (t) {
315+
const conf = {
316+
captureHeaders: true,
317+
sanitizeFieldNames: ['*session*', 'cookie'],
318+
};
319+
normalizeSanitizeFieldNames(conf);
320+
const req = {
321+
httpVersion: '1.1',
322+
method: 'GET',
323+
url: '/',
324+
headers: {
325+
host: 'example.com',
326+
cookie: 'foo=bar%3Bbaz; spam=eggs; sessionid=42',
327+
},
328+
};
329+
const parsed = parsers.getContextFromRequest(req, conf);
330+
t.deepEqual(parsed, {
331+
http_version: '1.1',
332+
method: 'GET',
333+
url: {
334+
raw: '/',
335+
protocol: 'http:',
336+
hostname: 'example.com',
337+
pathname: '/',
338+
full: 'http://example.com/',
339+
},
340+
headers: { host: 'example.com', cookie: '[REDACTED]' },
341+
});
342+
t.end();
343+
});
344+
282345
function getMockReq() {
283346
return {
284347
httpVersion: '1.1',

0 commit comments

Comments
 (0)