Add sanitization to log messages to reduce chance of tokens ending up in server logs

This commit is contained in:
Tom Moor
2023-03-13 20:42:08 -04:00
parent 1e2eb00ace
commit f6ac73a741

View File

@@ -1,6 +1,6 @@
import { IncomingMessage } from "http"; import { IncomingMessage } from "http";
import chalk from "chalk"; import chalk from "chalk";
import { isEmpty } from "lodash"; import { isEmpty, isArray, isObject, isString } from "lodash";
import winston from "winston"; import winston from "winston";
import env from "@server/env"; import env from "@server/env";
import Metrics from "@server/logging/Metrics"; import Metrics from "@server/logging/Metrics";
@@ -27,7 +27,7 @@ type Extra = Record<string, any>;
class Logger { class Logger {
output: winston.Logger; output: winston.Logger;
constructor() { public constructor() {
this.output = winston.createLogger({ this.output = winston.createLogger({
level: env.LOG_LEVEL, level: env.LOG_LEVEL,
}); });
@@ -54,8 +54,8 @@ class Logger {
* @param category A log message category that will be prepended * @param category A log message category that will be prepended
* @param extra Arbitrary data to be logged that will appear in prod logs * @param extra Arbitrary data to be logged that will appear in prod logs
*/ */
info(label: LogCategory, message: string, extra?: Extra) { public info(label: LogCategory, message: string, extra?: Extra) {
this.output.info(message, { ...extra, label }); this.output.info(message, { ...this.sanitize(extra), label });
} }
/** /**
@@ -64,8 +64,8 @@ class Logger {
* @param category A log message category that will be prepended * @param category A log message category that will be prepended
* @param extra Arbitrary data to be logged that will appear in prod logs * @param extra Arbitrary data to be logged that will appear in prod logs
*/ */
debug(label: LogCategory, message: string, extra?: Extra) { public debug(label: LogCategory, message: string, extra?: Extra) {
this.output.debug(message, { ...extra, label }); this.output.debug(message, { ...this.sanitize(extra), label });
} }
/** /**
@@ -74,7 +74,7 @@ class Logger {
* @param message A warning message * @param message A warning message
* @param extra Arbitrary data to be logged that will appear in prod logs * @param extra Arbitrary data to be logged that will appear in prod logs
*/ */
warn(message: string, extra?: Extra) { public warn(message: string, extra?: Extra) {
Metrics.increment("logger.warning"); Metrics.increment("logger.warning");
if (env.SENTRY_DSN) { if (env.SENTRY_DSN) {
@@ -90,7 +90,7 @@ class Logger {
} }
if (isProduction) { if (isProduction) {
this.output.warn(message, extra); this.output.warn(message, this.sanitize(extra));
} else if (extra) { } else if (extra) {
console.warn(message, extra); console.warn(message, extra);
} else { } else {
@@ -106,7 +106,7 @@ class Logger {
* @param extra Arbitrary data to be logged that will appear in prod logs * @param extra Arbitrary data to be logged that will appear in prod logs
* @param request An optional request object to attach to the error * @param request An optional request object to attach to the error
*/ */
error( public error(
message: string, message: string,
error: Error, error: Error,
extra?: Extra, extra?: Extra,
@@ -122,7 +122,7 @@ class Logger {
scope.setLevel("error"); scope.setLevel("error");
for (const key in extra) { for (const key in extra) {
scope.setExtra(key, extra[key]); scope.setExtra(key, this.sanitize(extra[key]));
} }
if (request) { if (request) {
@@ -147,6 +147,56 @@ class Logger {
}); });
} }
} }
/**
* Sanitize data attached to logs and errors to remove sensitive information.
*
* @param input The data to sanitize
* @returns The sanitized data
*/
private sanitize<T>(input: T): T {
// Short circuit if we're not in production to enable easier debugging
if (!isProduction) {
return input;
}
const sensitiveFields = [
"accessToken",
"refreshToken",
"token",
"password",
"content",
];
if (isString(input)) {
if (sensitiveFields.some((field) => input.includes(field))) {
return "[Filtered]" as T;
}
}
if (isArray(input)) {
return input.map(this.sanitize) as T;
}
if (isObject(input)) {
const output = { ...input };
for (const key of Object.keys(output)) {
if (isObject(output[key])) {
output[key] = this.sanitize(output[key]);
} else if (isArray(output[key])) {
output[key] = output[key].map(this.sanitize);
} else if (sensitiveFields.includes(key)) {
output[key] = "[Filtered]";
} else {
output[key] = this.sanitize(output[key]);
}
}
return output;
}
return input;
}
} }
export default new Logger(); export default new Logger();