@redthreadlabs/tracelog 1.7.0 → 1.9.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.
- package/lib/filters/sanitize-field-names.js +90 -0
- package/lib/parsers.js +9 -12
- package/package.json +1 -1
|
@@ -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,91 @@ function redactKeysFromPostedFormVariables(body, requestHeaders, regexes) {
|
|
|
42
46
|
return body;
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Redact sensitive fields from a captured request body, returning a
|
|
51
|
+
* structured value wherever the body is structured (since 1.9.0):
|
|
52
|
+
*
|
|
53
|
+
* - JSON content types (`application/json`, `application/*+json`, with or
|
|
54
|
+
* without a charset suffix) — **deep** redaction: any key at any depth
|
|
55
|
+
* matching the sanitizeFieldNames patterns is replaced, recursing through
|
|
56
|
+
* nested objects and arrays. The result is an **embedded object/array**,
|
|
57
|
+
* whether the body arrived parsed or as a JSON string. Strings that fail
|
|
58
|
+
* to parse are returned untouched as strings.
|
|
59
|
+
* - `application/x-www-form-urlencoded` — also returned as an embedded,
|
|
60
|
+
* deep-redacted object (extended parsers can nest); historically this
|
|
61
|
+
* re-serialized to a query string.
|
|
62
|
+
* - anything else — returned as-is.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object | String} body
|
|
65
|
+
* @param {Object} requestHeaders
|
|
66
|
+
* @param {Array<RegExp>} regexes
|
|
67
|
+
* @returns {Object | Array | String} redacted body, structured when possible
|
|
68
|
+
*/
|
|
69
|
+
function redactKeysFromBody(body, requestHeaders, regexes) {
|
|
70
|
+
// Operate even without patterns: redactDeep also normalizes circulars and
|
|
71
|
+
// pathological nesting, which must never reach the serializer.
|
|
72
|
+
const res = Array.isArray(regexes) ? regexes : [];
|
|
73
|
+
const contentType = String(requestHeaders['content-type'] || '');
|
|
74
|
+
const mime = contentType.split(';')[0].trim().toLowerCase();
|
|
75
|
+
|
|
76
|
+
// A body some middleware already parsed is structured data no matter what
|
|
77
|
+
// the content-type header claims — deep-redact and embed it.
|
|
78
|
+
if (body !== null && !Buffer.isBuffer(body) && typeof body === 'object') {
|
|
79
|
+
return redactDeep(body, res, 0, new WeakSet());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof body !== 'string') {
|
|
83
|
+
return body;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (mime === HEADER_FORM_URLENCODED) {
|
|
87
|
+
// querystring.parse returns a null-prototype object; copy to a plain one
|
|
88
|
+
return redactDeep({ ...querystring.parse(body) }, res, 0, new WeakSet());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isJsonContentType(contentType)) {
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(body);
|
|
95
|
+
} catch (_err) {
|
|
96
|
+
return body; // claimed JSON but isn't; leave it alone
|
|
97
|
+
}
|
|
98
|
+
return redactDeep(parsed, res, 0, new WeakSet());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return body;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isJsonContentType(contentType) {
|
|
105
|
+
// 'application/json', 'application/json; charset=utf-8',
|
|
106
|
+
// 'application/vnd.api+json', …
|
|
107
|
+
const mime = contentType.split(';')[0].trim().toLowerCase();
|
|
108
|
+
return mime === 'application/json' || mime.endsWith('+json');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function redactDeep(value, regexes, depth, seen) {
|
|
112
|
+
if (value === null || typeof value !== 'object') {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
if (depth >= MAX_REDACT_DEPTH || seen.has(value)) {
|
|
116
|
+
return REDACTED; // too deep / circular: redact rather than risk leaking
|
|
117
|
+
}
|
|
118
|
+
seen.add(value);
|
|
119
|
+
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
return value.map((item) => redactDeep(item, regexes, depth + 1, seen));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = {};
|
|
125
|
+
for (const key of Object.keys(value)) {
|
|
126
|
+
const shouldRedact = regexes.some((regex) => regex.test(key));
|
|
127
|
+
result[key] = shouldRedact
|
|
128
|
+
? REDACTED
|
|
129
|
+
: redactDeep(value[key], regexes, depth + 1, seen);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
45
134
|
/**
|
|
46
135
|
* Returns a copy of the provided object. Each entry of the copy will have
|
|
47
136
|
* its value REDACTEd if the key matches any of the regexes
|
|
@@ -66,4 +155,5 @@ function redactKeysFromObject(obj, regexes, redactedStr = REDACTED) {
|
|
|
66
155
|
module.exports = {
|
|
67
156
|
redactKeysFromObject,
|
|
68
157
|
redactKeysFromPostedFormVariables,
|
|
158
|
+
redactKeysFromBody,
|
|
69
159
|
};
|
package/lib/parsers.js
CHANGED
|
@@ -12,12 +12,11 @@ const basicAuth = require('basic-auth');
|
|
|
12
12
|
const getUrlFromRequest = require('original-url');
|
|
13
13
|
const parseHttpHeadersFromReqOrRes = require('http-headers');
|
|
14
14
|
const cookie = require('cookie');
|
|
15
|
-
const stringify = require('fast-safe-stringify');
|
|
16
15
|
|
|
17
16
|
const REDACTED = require('./constants').REDACTED;
|
|
18
17
|
const {
|
|
19
18
|
redactKeysFromObject,
|
|
20
|
-
|
|
19
|
+
redactKeysFromBody,
|
|
21
20
|
} = require('./filters/sanitize-field-names');
|
|
22
21
|
|
|
23
22
|
// When redacting individual cookie field values, this string is used instead
|
|
@@ -97,23 +96,26 @@ function getContextFromRequest(req, conf, type) {
|
|
|
97
96
|
var haveBody = body && (chunked || contentLength > 0);
|
|
98
97
|
|
|
99
98
|
if (haveBody) {
|
|
99
|
+
const bodyContentType = String(req.headers['content-type'] || '');
|
|
100
100
|
if (!captureBody) {
|
|
101
101
|
context.body = '[REDACTED]';
|
|
102
|
+
} else if (bodyContentType.split(';')[0].trim().toLowerCase().startsWith('multipart/')) {
|
|
103
|
+
// File uploads: never record the raw multipart body — it can embed
|
|
104
|
+
// entire file contents and no per-field redaction applies to it.
|
|
105
|
+
context.body = '[REDACTED: multipart body]';
|
|
102
106
|
} else if (Buffer.isBuffer(body)) {
|
|
103
107
|
context.body = '<Buffer>';
|
|
104
108
|
} else {
|
|
105
109
|
if (typeof body === 'string' && req.bodyIsBase64Encoded === true) {
|
|
106
110
|
body = Buffer.from(body, 'base64').toString('utf8');
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
// Structured bodies (JSON, form-encoded) come back as deep-redacted
|
|
113
|
+
// objects and are recorded embedded, not stringified (since 1.9.0).
|
|
114
|
+
context.body = redactKeysFromBody(
|
|
109
115
|
body,
|
|
110
116
|
req.headers,
|
|
111
117
|
conf.sanitizeFieldNamesRegExp,
|
|
112
118
|
);
|
|
113
|
-
if (typeof body !== 'string') {
|
|
114
|
-
body = tryJsonStringify(body) || stringify(body);
|
|
115
|
-
}
|
|
116
|
-
context.body = body;
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -211,11 +213,6 @@ function parseUrl(urlStr) {
|
|
|
211
213
|
return new url.URL(urlStr, 'relative:///');
|
|
212
214
|
}
|
|
213
215
|
|
|
214
|
-
function tryJsonStringify(obj) {
|
|
215
|
-
try {
|
|
216
|
-
return JSON.stringify(obj);
|
|
217
|
-
} catch (e) {}
|
|
218
|
-
}
|
|
219
216
|
|
|
220
217
|
module.exports = {
|
|
221
218
|
getContextFromRequest,
|