NodeJS Best Practices: Redacting Secrets from Your Pino Logs

March 30, 2024

In today's world where data breaches are increasingly common, secure logging is not just important - it's absolutely essential. One area that developers often overlook is the logging of sensitive data, like user credentials or secrets. Such data leaks can lead to serious security vulnerabilities and violations of GDPR compliance if you're not careful.

This article will guide you through redacting such sensitive data from your logs when using Pino, a performant and lightweight logger for Node.js, aiding your application in maintaining GDPR compliance and reducing the risk of privileging escalation.

Pino

When instantiating a pino instance, you usually start with a similar piece of code:

// You can find the Gist here: https://gist.github.com/Lp-Francois/3d1b36907de8283a8a3eedca31c9cfc3

// file:`example.js`
// install packages: `npm i pino-http --save`
'use strict'

const pino = require('pino-http')
const http = require('http')
const server = http.createServer(handle)

const logger = pino({
  base: undefined, // removes pid and hostname from the logs
})

function handle (req, res) {
  logger(req, res)
  req.log.info('my request')
  res.end('hello world')
}

server.listen(3000, () => console.log('[i] running on http://localhost:3000'))

and when running it, then sending requests to it, you can

# terminal window 1
$ node example.js
# terminal window 2
$ curl http://localhost:3000 -H "authorization: bearer my-super-secret" -H "Content-Type: application/json"

# logs in terminal 1:
[i] running on http://localhost:3000
{"level":30,"time":1711808000315,"req":{"id":1,"method":"GET","url":"/","headers":{"host":"localhost:3000","user-agent":"curl/8.4.0","accept":"*/*","authorization":"bearer my-super-secret","content-type":"application/json"},"remoteAddress":"::1","remotePort":63547},"msg":"my request"}
{"level":30,"time":1711808000322,"req":{"id":1,"method":"GET","url":"/","headers":{"host":"localhost:3000","user-agent":"curl/8.4.0","accept":"*/*","authorization":"bearer my-super-secret","content-type":"application/json"},"remoteAddress":"::1","remotePort":63547},"res":{"statusCode":200,"headers":{}},"responseTime":7,"msg":"request completed"}

Notice the authorization header containing the secret in the logs 😢: ..."accept":"*/*","authorization":"bearer my-super-secret"...

Redaction

Pino has a "hidden" (hard to find) documentation on redacting secrets.

It is possible to pass to your pino logger an array containing paths to keys holding sensible data.

To remove our authorization header from the req object, we just have to append this to our config: redact: ['req.headers.authorization'].

Here is the full example:

'use strict'

const pino = require('pino-http')
const http = require('http')
const server = http.createServer(handle)

const logger = pino({
  base: undefined,
  // ⚠️ The next line is the important one:
  redact: [
    'req.headers.authorization',
  ]
})

function handle (req, res) {
  logger(req, res)
  req.log.info('my request')
  res.end('hello world')
}

server.listen(3000, () => console.log('running on http://localhost:3000'))

Outputing the following logs:

[i] running on http://localhost:3000
{"level":30,"time":1711824390585,"req":{"id":1,"method":"GET","url":"/","headers":{"host":"localhost:3000","user-agent":"curl/8.4.0","accept":"*/*","authorization":"[Redacted]","content-type":"application/json"},"remoteAddress":"::1","remotePort":58055},"msg":"my request"}
{"level":30,"time":1711824390594,"req":{"id":1,"method":"GET","url":"/","headers":{"host":"localhost:3000","user-agent":"curl/8.4.0","accept":"*/*","authorization":"[Redacted]","content-type":"application/json"},"remoteAddress":"::1","remotePort":58055},"res":{"statusCode":200,"headers":{}},"responseTime":9,"msg":"request completed"}

Notice the authorization header now has a [Redacted] value!

Note the keys are case sensitive! If you want to add to the redact list an uppercase header like req.headers.CREDENTIALS be careful as it would ignore the lowercase credentials header.

It is also an option to drop both value and key (redact.remove), or change the value of the redacted field to something else with the censor option (I'll let you read the documentation for that 😉).

I didn't find many resources online that covered this tip specifically, so I hope this insight proves valuable to you!

Happy safe logging with Pino!