@redthreadlabs/tracelog 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,10 @@ const querystring = require('querystring');
10
10
  const HEADER_FORM_URLENCODED = 'application/x-www-form-urlencoded';
11
11
  const REDACTED = require('../constants').REDACTED;
12
12
 
13
+ // Depth guard for deep JSON redaction. Far deeper than any sane request
14
+ // body; protects against adversarial nesting.
15
+ const MAX_REDACT_DEPTH = 32;
16
+
13
17
  /**
14
18
  * Handles req.body as object or string
15
19
  *
@@ -42,6 +46,80 @@ function redactKeysFromPostedFormVariables(body, requestHeaders, regexes) {
42
46
  return body;
43
47
  }
44
48
 
49
+ /**
50
+ * Redact sensitive fields from a captured request body of any supported
51
+ * content type:
52
+ *
53
+ * - `application/x-www-form-urlencoded` — top-level form fields (the
54
+ * historical Elastic APM behavior).
55
+ * - JSON content types (`application/json`, `application/*+json`, with or
56
+ * without a charset suffix) — **deep** redaction: any key at any depth
57
+ * matching the sanitizeFieldNames patterns is replaced, recursing through
58
+ * nested objects and arrays. Stringified JSON bodies are parsed, redacted,
59
+ * and re-stringified; strings that fail to parse are returned untouched.
60
+ * - anything else — returned as-is.
61
+ *
62
+ * @param {Object | String} body
63
+ * @param {Object} requestHeaders
64
+ * @param {Array<RegExp>} regexes
65
+ * @returns {Object | String} a copy of the body with redacted fields
66
+ */
67
+ function redactKeysFromBody(body, requestHeaders, regexes) {
68
+ const contentType = String(requestHeaders['content-type'] || '');
69
+ if (!isJsonContentType(contentType)) {
70
+ return redactKeysFromPostedFormVariables(body, requestHeaders, regexes);
71
+ }
72
+ if (!Array.isArray(regexes)) {
73
+ return body;
74
+ }
75
+
76
+ if (body !== null && !Buffer.isBuffer(body) && typeof body === 'object') {
77
+ return redactDeep(body, regexes, 0, new WeakSet());
78
+ }
79
+
80
+ if (typeof body === 'string') {
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(body);
84
+ } catch (_err) {
85
+ return body; // claimed JSON but isn't; leave it alone
86
+ }
87
+ return JSON.stringify(redactDeep(parsed, regexes, 0, new WeakSet()));
88
+ }
89
+
90
+ return body;
91
+ }
92
+
93
+ function isJsonContentType(contentType) {
94
+ // 'application/json', 'application/json; charset=utf-8',
95
+ // 'application/vnd.api+json', …
96
+ const mime = contentType.split(';')[0].trim().toLowerCase();
97
+ return mime === 'application/json' || mime.endsWith('+json');
98
+ }
99
+
100
+ function redactDeep(value, regexes, depth, seen) {
101
+ if (value === null || typeof value !== 'object') {
102
+ return value;
103
+ }
104
+ if (depth >= MAX_REDACT_DEPTH || seen.has(value)) {
105
+ return REDACTED; // too deep / circular: redact rather than risk leaking
106
+ }
107
+ seen.add(value);
108
+
109
+ if (Array.isArray(value)) {
110
+ return value.map((item) => redactDeep(item, regexes, depth + 1, seen));
111
+ }
112
+
113
+ const result = {};
114
+ for (const key of Object.keys(value)) {
115
+ const shouldRedact = regexes.some((regex) => regex.test(key));
116
+ result[key] = shouldRedact
117
+ ? REDACTED
118
+ : redactDeep(value[key], regexes, depth + 1, seen);
119
+ }
120
+ return result;
121
+ }
122
+
45
123
  /**
46
124
  * Returns a copy of the provided object. Each entry of the copy will have
47
125
  * its value REDACTEd if the key matches any of the regexes
@@ -66,4 +144,5 @@ function redactKeysFromObject(obj, regexes, redactedStr = REDACTED) {
66
144
  module.exports = {
67
145
  redactKeysFromObject,
68
146
  redactKeysFromPostedFormVariables,
147
+ redactKeysFromBody,
69
148
  };
package/lib/parsers.js CHANGED
@@ -17,7 +17,7 @@ const stringify = require('fast-safe-stringify');
17
17
  const REDACTED = require('./constants').REDACTED;
18
18
  const {
19
19
  redactKeysFromObject,
20
- redactKeysFromPostedFormVariables,
20
+ redactKeysFromBody,
21
21
  } = require('./filters/sanitize-field-names');
22
22
 
23
23
  // When redacting individual cookie field values, this string is used instead
@@ -105,7 +105,7 @@ function getContextFromRequest(req, conf, type) {
105
105
  if (typeof body === 'string' && req.bodyIsBase64Encoded === true) {
106
106
  body = Buffer.from(body, 'base64').toString('utf8');
107
107
  }
108
- body = redactKeysFromPostedFormVariables(
108
+ body = redactKeysFromBody(
109
109
  body,
110
110
  req.headers,
111
111
  conf.sanitizeFieldNamesRegExp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redthreadlabs/tracelog",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Node.js APM instrumentation that writes traces to JSONL files",
5
5
  "publishConfig": {
6
6
  "access": "public"