diff --git a/browser.js b/browser.js index 8c5ed37e5..76dbae289 100644 --- a/browser.js +++ b/browser.js @@ -1,6 +1,7 @@ 'use strict' const format = require('quick-format-unescaped') +const Redact = require('@pinojs/redact') module.exports = pino @@ -88,6 +89,9 @@ function pino (opts) { proto[level] = proto }) } + const redact = opts.redact + const redactFn = redact ? createRedactFn(redact) : null + if (opts.enabled === false || opts.browser.disabled) opts.level = 'silent' const level = opts.level || 'info' const logger = Object.create(proto) @@ -112,6 +116,7 @@ function pino (opts) { asObjectBindingsOnly: opts.browser.asObjectBindingsOnly, formatters: opts.browser.formatters, reportCaller: opts.browser.reportCaller, + redactFn, levels, timestamp: getTimeFunction(opts), messageKey: opts.messageKey || 'msg', @@ -333,8 +338,18 @@ function createWrap (self, opts, rootLogger, level) { if (caller) out[0].caller = caller } catch (e) {} } + if (opts.redactFn && out.length > 0 && out[0] && typeof out[0] === 'object') { + out[0] = opts.redactFn(out[0]) + } write.call(proto, ...out) } else { + if (opts.redactFn) { + for (var j = 0; j < args.length; j++) { + if (args[j] !== null && typeof args[j] === 'object' && !Array.isArray(args[j])) { + args[j] = opts.redactFn(args[j]) + } + } + } if (opts.reportCaller) { try { const caller = getCallerLocation() @@ -488,6 +503,27 @@ function getTimeFunction (opts) { return epochTime } +function createRedactFn (redact) { + const paths = Array.isArray(redact) ? redact : redact.paths + if (!Array.isArray(paths)) { throw Error('pino: redact.paths must be an array of strings') } + const redactOpts = { paths, serialize: false } + if (typeof redact === 'object' && !Array.isArray(redact)) { + if (redact.censor !== undefined) redactOpts.censor = redact.censor + if (redact.remove === true) redactOpts.remove = true + } + const redactor = Redact(redactOpts) + return function redactFn (obj) { + const redacted = redactor(obj) + if (typeof redacted.restore === 'function') { + const result = Object.assign({}, redacted) + delete result.restore + redacted.restore() + return result + } + return redacted + } +} + function mock () { return {} } function passthrough (a) { return a } function noop () {} diff --git a/docs/redaction.md b/docs/redaction.md index 9b7e4ff09..64eb8fece 100644 --- a/docs/redaction.md +++ b/docs/redaction.md @@ -1,7 +1,5 @@ # Redaction -> Redaction is not supported in the browser [#670](https://github.com/pinojs/pino/issues/670) - To redact sensitive information, supply paths to keys that hold sensitive data using the `redact` option. Note that paths that contain hyphens need to use brackets to access the hyphenated property: @@ -130,6 +128,3 @@ the [`fast-redact` benchmarks](https://github.com/davidmarkclements/fast-redact# The `redact` option is intended as an initialization time configuration option. Path strings must not originate from user input. -The `fast-redact` module uses a VM context to syntax check the paths, user input -should never be combined with such an approach. See the [`fast-redact` Caveat](https://github.com/davidmarkclements/fast-redact#caveat) -and the [`fast-redact` Approach](https://github.com/davidmarkclements/fast-redact#approach) for in-depth information. diff --git a/test/browser-redact.test.js b/test/browser-redact.test.js new file mode 100644 index 000000000..6d1bcb08d --- /dev/null +++ b/test/browser-redact.test.js @@ -0,0 +1,185 @@ +'use strict' +const test = require('tape') +const pino = require('../browser') + +test('redact option with array of paths', ({ end, is, ok }) => { + const instance = pino({ + redact: ['password'], + browser: { + write (o) { + is(o.user, 'test') + is(o.password, '[REDACTED]') + ok(o.time) + } + } + }) + instance.info({ user: 'test', password: 'secret' }) + end() +}) + +test('redact option with nested paths', ({ end, is }) => { + const instance = pino({ + redact: ['user.password'], + browser: { + write (o) { + is(o.user.name, 'John') + is(o.user.password, '[REDACTED]') + } + } + }) + instance.info({ user: { name: 'John', password: 'secret' } }) + end() +}) + +test('redact option with custom censor', ({ end, is }) => { + const instance = pino({ + redact: { paths: ['password'], censor: '***' }, + browser: { + write (o) { + is(o.password, '***') + } + } + }) + instance.info({ password: 'secret' }) + end() +}) + +test('redact option with remove', ({ end, is, ok }) => { + const instance = pino({ + redact: { paths: ['password'], remove: true }, + browser: { + write (o) { + is(o.user, 'test') + ok(!('password' in o)) + } + } + }) + instance.info({ user: 'test', password: 'secret' }) + end() +}) + +test('redact option with multiple paths', ({ end, is }) => { + const instance = pino({ + redact: ['password', 'secret', 'token'], + browser: { + write (o) { + is(o.user, 'test') + is(o.password, '[REDACTED]') + is(o.secret, '[REDACTED]') + is(o.token, '[REDACTED]') + } + } + }) + instance.info({ user: 'test', password: 'a', secret: 'b', token: 'c' }) + end() +}) + +test('redact does not affect non-matching properties', ({ end, is }) => { + const instance = pino({ + redact: ['password'], + browser: { + write (o) { + is(o.user, 'test') + is(o.email, 'test@test.com') + is(o.password, '[REDACTED]') + } + } + }) + instance.info({ user: 'test', email: 'test@test.com', password: 'secret' }) + end() +}) + +test('redact works with child loggers', ({ end, is }) => { + const instance = pino({ + redact: ['password'], + browser: { + write (o) { + is(o.service, 'auth') + is(o.password, '[REDACTED]') + } + } + }) + const child = instance.child({ service: 'auth' }) + child.info({ password: 'secret' }) + end() +}) + +test('redact works with wildcard paths', ({ end, is }) => { + const instance = pino({ + redact: ['users[*].password'], + browser: { + write (o) { + is(o.users[0].name, 'John') + is(o.users[0].password, '[REDACTED]') + is(o.users[1].name, 'Jane') + is(o.users[1].password, '[REDACTED]') + } + } + }) + instance.info({ + users: [ + { name: 'John', password: 'pass1' }, + { name: 'Jane', password: 'pass2' } + ] + }) + end() +}) + +test('redact in non-asObject mode redacts object arguments', ({ end, is }) => { + const info = console.info + console.info = function (obj) { + is(obj.password, '[REDACTED]') + is(obj.user, 'test') + console.info = info + } + const instance = pino({ + level: 'info', + redact: ['password'] + }) + instance.info({ user: 'test', password: 'secret' }) + end() +}) + +test('redact with formatters', ({ end, is }) => { + const instance = pino({ + redact: ['password'], + browser: { + formatters: { + level (label, number) { + return { label, level: number } + } + }, + write (o) { + is(o.label, 'info') + is(o.password, '[REDACTED]') + } + } + }) + instance.info({ password: 'secret' }) + end() +}) + +test('redact throws if paths is not an array', ({ end, throws }) => { + throws(function () { + pino({ redact: { paths: 'not-an-array' } }) + }) + end() +}) + +test('redact with censor function', ({ end, is }) => { + const instance = pino({ + redact: { + paths: ['password'], + censor: function (value) { + return value[0] + '***' + } + }, + browser: { + write (o) { + is(o.password, 's***') + } + } + }) + instance.info({ password: 'secret' }) + end() +})