Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions browser.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const format = require('quick-format-unescaped')
const Redact = require('@pinojs/redact')

module.exports = pino

Expand Down Expand Up @@ -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)
Expand All @@ -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',
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 () {}
Expand Down
5 changes: 0 additions & 5 deletions docs/redaction.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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.
185 changes: 185 additions & 0 deletions test/browser-redact.test.js
Original file line number Diff line number Diff line change
@@ -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()
})