How to hide sensitive logs in Node.js
Accidentally logging passwords, tokens, or personal data is a serious security risk that can expose sensitive information in log aggregators, monitoring tools, and stdout captures. As the creator of CoreUI with 25 years of backend development experience, I’ve seen production incidents caused by tokens appearing in plain-text logs. The safest approach is to use a structured logger like Pino or Winston with built-in redaction support that strips sensitive fields before they’re ever written. This ensures credentials never appear in logs regardless of which developer added the log statement.
Use Pino’s redact option to automatically remove sensitive fields from all log output.
import pino from 'pino'
const logger = pino({
redact: {
paths: [
'password',
'token',
'accessToken',
'refreshToken',
'authorization',
'req.headers.authorization',
'req.headers.cookie',
'body.password',
'body.creditCard',
'*.secret'
],
censor: '[REDACTED]'
}
})
// ❌ Would log the actual password without redaction
logger.info({ body: { username: 'alice', password: 's3cr3t' } }, 'Login attempt')
// ✅ Output: {"body":{"username":"alice","password":"[REDACTED]"},"msg":"Login attempt"}
Pino’s redact option uses fast-redact under the hood to replace matching path values with the censor string. The redaction happens during serialization, before the log is written anywhere. Use wildcard paths like '*.secret' to redact fields named secret at any depth.
Redacting Nested and Array Paths
Target deeply nested sensitive values with bracket notation.
const logger = pino({
redact: {
paths: [
'user.password',
'user.ssn',
'payment.cardNumber',
'payment.cvv',
'users[*].password',
'items[*].apiKey'
],
censor: '[REMOVED]'
}
})
logger.info({
users: [
{ id: 1, name: 'Alice', password: 'secret' },
{ id: 2, name: 'Bob', password: 'pass123' }
]
}, 'Fetched users')
// passwords are replaced with [REMOVED] in all array elements
The [*] wildcard matches every element in an array. This is essential when logging collections of user objects where each item may contain sensitive fields.
Environment-Based Log Level Control
Disable verbose logging in production to reduce the attack surface.
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL ?? (process.env.NODE_ENV === 'production' ? 'warn' : 'debug'),
redact: ['password', 'token', 'authorization']
})
// Only logged in development (debug level)
logger.debug({ query: req.query }, 'Incoming request')
// Always logged
logger.warn({ userId }, 'Failed login attempt')
logger.error({ err }, 'Unhandled error')
Setting level: 'warn' in production means debug and info logs are never written, reducing both log volume and the chance of sensitive data appearing in verbose messages.
Custom Serializers for Request Objects
Sanitize Express/Fastify request objects before they’re logged.
const logger = pino({
serializers: {
req(req) {
return {
method: req.method,
url: req.url,
// Omit headers entirely, or whitelist safe ones
headers: {
'user-agent': req.headers['user-agent'],
'content-type': req.headers['content-type']
}
// Never log req.body here - it may contain passwords
}
},
res(res) {
return {
statusCode: res.statusCode
}
}
}
})
Custom serializers let you control exactly what shape gets logged for complex objects. Whitelisting safe headers rather than blacklisting dangerous ones is safer — new headers added in future won’t accidentally leak.
Best Practice Note
This is the same logging strategy we use in CoreUI backend services — all sensitive fields are redacted at the logger configuration level, not at individual call sites. Never rely on developers remembering to omit sensitive data from log calls. Centralizing redaction rules in the logger configuration means the rules apply automatically to every log statement in the codebase. Review your redaction list whenever you add new data models with personal or financial information.



