-
Notifications
You must be signed in to change notification settings - Fork 225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: sanitize field names #1898
Changes from all commits
d48f320
ea2df4e
e54e1bc
45fd126
2ae3bcc
154685d
80f886c
3931b39
f02c413
27495ec
a501ea2
7717e6d
4746d29
0bdc12d
bb7e852
c5f562e
ab80a07
f1b5e24
56351d8
4050379
1bf733d
819c3b8
6958a65
0bba0ee
dc4169c
12a6e6d
d049c29
58f3af7
b9735f3
a2b8b21
5dfc498
4ad6433
56f91c1
295d76e
38388f8
b90b973
ae3b34c
bea04d9
ca4f97f
435cf80
a851b08
7b333f1
545571e
7261952
2d0cf94
6424cde
a78bbb3
48fd4ee
ea40071
18c6d58
14066f7
2dc2d6f
b831268
04769bf
03f5a7f
eb44700
0ce1520
026f6df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,6 +66,10 @@ var DEFAULTS = { | |
logUncaughtExceptions: false, // TODO: Change to `true` in the v4.0.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Everything in this file is just configuration handling (including converting the wildcards into regular expressions) |
||
metricsInterval: '30s', | ||
metricsLimit: 1000, | ||
sanitizeFieldNames: ['password', 'passwd', 'pwd', 'secret', '*key', '*token*', | ||
'*session*', '*credit*', '*card*', 'authorization', 'set-cookie', | ||
'pw', 'pass', 'connect.sid' | ||
], | ||
serviceNodeName: undefined, | ||
serverTimeout: '30s', | ||
sourceLinesErrorAppFrames: 5, | ||
|
@@ -117,6 +121,7 @@ var ENV_TABLE = { | |
metricsInterval: 'ELASTIC_APM_METRICS_INTERVAL', | ||
metricsLimit: 'ELASTIC_APM_METRICS_LIMIT', | ||
payloadLogFile: 'ELASTIC_APM_PAYLOAD_LOG_FILE', | ||
sanitizeFieldNames: 'ELASTIC_SANITIZE_FIELD_NAMES', | ||
serverCaCertFile: 'ELASTIC_APM_SERVER_CA_CERT_FILE', | ||
secretToken: 'ELASTIC_APM_SECRET_TOKEN', | ||
serverTimeout: 'ELASTIC_APM_SERVER_TIMEOUT', | ||
|
@@ -142,7 +147,8 @@ var CENTRAL_CONFIG = { | |
transaction_sample_rate: 'transactionSampleRate', | ||
transaction_max_spans: 'transactionMaxSpans', | ||
capture_body: 'captureBody', | ||
transaction_ignore_urls: 'transactionIgnoreUrls' | ||
transaction_ignore_urls: 'transactionIgnoreUrls', | ||
sanitize_field_names: 'sanitizeFieldNames' | ||
} | ||
|
||
var VALIDATORS = { | ||
|
@@ -195,6 +201,7 @@ var MINUS_ONE_EQUAL_INFINITY = [ | |
|
||
var ARRAY_OPTS = [ | ||
'disableInstrumentations', | ||
'sanitizeFieldNames', | ||
'transactionIgnoreUrls' | ||
] | ||
|
||
|
@@ -214,6 +221,7 @@ class Config { | |
this.ignoreUserAgentStr = [] | ||
this.ignoreUserAgentRegExp = [] | ||
this.transactionIgnoreUrlRegExp = [] | ||
this.sanitizeFieldNamesRegExp = [] | ||
// If we didn't find a config file on process boot, but a path to one is | ||
// provided as a config option, let's instead try to load that | ||
if (confFile === null && opts && opts.configFile) { | ||
|
@@ -408,9 +416,20 @@ function normalize (opts) { | |
normalizeTime(opts) | ||
normalizeBools(opts) | ||
normalizeIgnoreOptions(opts) | ||
normalizeSanitizeFieldNames(opts) | ||
truncateOptions(opts) | ||
} | ||
|
||
function normalizeSanitizeFieldNames (opts) { | ||
if (opts.sanitizeFieldNames) { | ||
const wildcard = new WildcardMatcher() | ||
for (const ptn of opts.sanitizeFieldNames) { | ||
const re = wildcard.compile(ptn) | ||
opts.sanitizeFieldNamesRegExp.push(re) | ||
} | ||
} | ||
} | ||
|
||
function normalizeIgnoreOptions (opts) { | ||
if (opts.transactionIgnoreUrls) { | ||
// We can't guarantee that opts will be a Config so set a | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* Central location for shared constants | ||
*/ | ||
|
||
module.exports = { | ||
REDACTED: '[REDACTED]' | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
'use strict' | ||
|
||
const REDACTED = '[REDACTED]' | ||
const REDACTED = require('../constants').REDACTED | ||
|
||
const cookie = require('cookie') | ||
const redact = require('../redact-secrets')(REDACTED) | ||
|
@@ -12,18 +12,28 @@ function httpHeaders (obj) { | |
const requestHeaders = obj.context && obj.context.request && obj.context.request.headers | ||
const responseHeaders = obj.context && obj.context.response && obj.context.response.headers | ||
|
||
if (requestHeaders) filterSensitiveHeaders(requestHeaders) | ||
if (responseHeaders) filterSensitiveHeaders(responseHeaders) | ||
if (requestHeaders) filterCookieHeaders(requestHeaders) | ||
if (responseHeaders) filterCookieHeaders(responseHeaders) | ||
|
||
return obj | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We changed the handling of undefined in this module to be consistent across our all redacted header fields. The was prompted by the fact that
|
||
function filterSensitiveHeaders (headers) { | ||
/** | ||
* Filters cookie _values_ in http headers | ||
* | ||
* The filterCookieHeaders method filters individual | ||
* cookie values | ||
*/ | ||
function filterCookieHeaders (headers) { | ||
for (const key in headers) { | ||
astorm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// undefined headers will be dropped when serialized as | ||
// json, and don't need to be redacted. If a cookie | ||
// header is already redacted there's no need to parse its | ||
// values. | ||
if (headers[key] === undefined || REDACTED === headers[key]) { | ||
astorm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
continue | ||
} | ||
switch (key.toLowerCase()) { | ||
case 'authorization': | ||
headers[key] = REDACTED | ||
break | ||
case 'cookie': | ||
if (typeof headers[key] === 'string') { | ||
const cookies = cookie.parse(headers[key]) | ||
|
@@ -34,16 +44,17 @@ function filterSensitiveHeaders (headers) { | |
} | ||
break | ||
case 'set-cookie': | ||
if (typeof headers[key] !== 'undefined') { | ||
try { | ||
const setCookies = new SetCookie(headers[key]) | ||
redact.forEach(setCookies) | ||
headers[key] = stringify(setCookies) | ||
} catch (err) { | ||
// Ignore error | ||
headers[key] = '[malformed set-cookie header]' | ||
} | ||
// if the sanitize_field_names module has redacted this there's | ||
// no need to attempt the sanitizaion of individual cookies | ||
try { | ||
const setCookies = new SetCookie(headers[key]) | ||
redact.forEach(setCookies) | ||
headers[key] = stringify(setCookies) | ||
} catch (err) { | ||
// Ignore error | ||
headers[key] = '[malformed set-cookie header]' | ||
} | ||
|
||
break | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
'use strict' | ||
const querystring = require('querystring') | ||
|
||
const HEADER_FORM_URLENCODED = 'application/x-www-form-urlencoded' | ||
const REDACTED = require('../constants').REDACTED | ||
|
||
/** | ||
* Handles req.body as object or string | ||
* | ||
* Express provides multiple body parser middlewares with x-www-form-urlencoded | ||
* handling. See http://expressjs.com/en/resources/middleware/body-parser.html | ||
*/ | ||
function redactKeysFromPostedFormVariables (body, requestHeaders, regexes) { | ||
// only redact from application/x-www-form-urlencoded | ||
if (HEADER_FORM_URLENCODED !== requestHeaders['content-type']) { | ||
return body | ||
} | ||
|
||
// if body is a plain object, use redactKeysFromObject | ||
if (body !== null && !Buffer.isBuffer(body) && typeof body === 'object') { | ||
return redactKeysFromObject(body, regexes) | ||
} | ||
|
||
// if body is a string, use querystring to create object, | ||
// pass to redactKeysFromObject, and reserialize as string | ||
if (typeof body === 'string') { | ||
const objBody = querystring.parse(body) | ||
redactKeysFromObject(objBody, regexes) | ||
return querystring.stringify(objBody) | ||
} | ||
|
||
return body | ||
} | ||
|
||
function redactKeyFromObject (obj, regex) { | ||
for (const [key] of Object.entries(obj)) { | ||
if (regex.test(key)) { | ||
obj[key] = REDACTED | ||
} | ||
} | ||
return obj | ||
} | ||
|
||
function redactKeysFromObject (obj, regexes) { | ||
if (!obj || !Array.isArray(regexes)) { | ||
return obj | ||
} | ||
for (const [, regex] of regexes.entries()) { | ||
redactKeyFromObject(obj, regex) | ||
} | ||
return obj | ||
} | ||
|
||
function createFilter (conf) { | ||
return sanitizeFieldNames | ||
|
||
function sanitizeFieldNames (obj) { | ||
const requestHeaders = obj.context && obj.context.request && obj.context.request.headers | ||
const responseHeaders = obj.context && obj.context.response && obj.context.response.headers | ||
const body = obj.context && obj.context.request && obj.context.request.body | ||
|
||
redactKeysFromObject(requestHeaders, conf.sanitizeFieldNamesRegExp) | ||
redactKeysFromObject(responseHeaders, conf.sanitizeFieldNamesRegExp) | ||
|
||
if (body) { | ||
obj.context.request.body = redactKeysFromPostedFormVariables(body, requestHeaders, conf.sanitizeFieldNamesRegExp) | ||
} | ||
|
||
return obj | ||
} | ||
} | ||
|
||
module.exports = { | ||
createFilter, | ||
redactKeysFromObject, | ||
redactKeysFromPostedFormVariables | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Express has an external body parser middleware module for capturing
application/x-www-form-urlencoded
form bodies, so we run its tests through TAV.Restify and HAPI do not use external middleware for capturing
application/x-www-form-urlencoded
form bodies, so there's no module to run through TAV.Fastify and koa do use external middleware for capturing
application/x-www-form-urlencoded
bodies, but the agent doesn't currently capture their request bodies, so we don't test those module (yet!)