Sensitive Data
The logging security problem
Logs are stored, searched, and sometimes shared. If a log entry contains a password, anyone with log access has the password. If it contains a JWT, anyone can impersonate the user. If it contains a credit card number, you violate PCI compliance.
The What to Log lesson listed what NOT to log. This lesson builds redaction — automatically removing or masking sensitive data from log entries.
What must never appear in logs
Passwords — Not even hashed. Log “login attempt for user alice”, not “login attempt with password s3cret”.
Authentication tokens — JWTs, session IDs, API keys. Log “token validated for user-42”, not the actual token.
Credit card numbers — PCI-DSS requires it. Log “payment processed for $29.99”, not the card number.
Personal Identifiable Information (PII) — Email addresses, phone numbers, physical addresses. Some regulations (GDPR) restrict logging PII.
Request/response bodies — Bodies can contain any of the above. Do not log bodies by default.
A redaction function
const SENSITIVE_KEYS = new Set([
"password",
"confirmPassword",
"currentPassword",
"newPassword",
"token",
"accessToken",
"refreshToken",
"sessionId",
"apiKey",
"authorization",
"creditCard",
"cardNumber",
"cvv",
"ssn",
"secret",
"privateKey",
]);
export function redact(obj: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.has(key.toLowerCase())) {
result[key] = "[REDACTED]";
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
result[key] = redact(value as Record<string, unknown>);
} else {
result[key] = value;
}
}
return result;
} redact({ username: "alice", password: "s3cret", token: "eyJ..." });
// { username: "alice", password: "[REDACTED]", token: "[REDACTED]" }
redact({ user: { email: "[email protected]", apiKey: "sk_live_..." } });
// { user: { email: "[email protected]", apiKey: "[REDACTED]" } } Integrating redaction into the logger
export class Logger {
private log(level: Level, message: string, context: Record<string, unknown> = {}): void {
if (LEVELS[level] < this.minLevel) return;
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...this.baseContext,
...redact(context), // ← Redact context before serializing
};
const output = JSON.stringify(entry);
// ... write to stdout/stderr
}
} Now every log call automatically redacts sensitive fields. A developer who accidentally logs { userId: "u1", password: "s3cret" } gets { userId: "u1", password: "[REDACTED]" } in the output.
Masking instead of redacting
Sometimes you want to show part of a value — enough to identify it, not enough to use it:
function mask(value: string, visibleChars: number = 4): string {
if (value.length <= visibleChars) return "****";
return "****" + value.slice(-visibleChars);
}
mask("sk_live_abc123def456"); // "****f456"
mask("[email protected]"); // "****e.com" (still somewhat PII though) Use masking for values where partial visibility helps debugging (last 4 digits of a card, last 4 chars of an API key). Use full redaction for passwords and tokens.
Preventing body logging
The Request-Response Logging lesson mentioned: do not log request bodies by default. Enforce this:
// DANGEROUS — body might contain passwords
logger.info("request received", { body: requestBody }); // DON'T
// SAFE — log only the fields you need
logger.info("request received", {
method: request.method,
path: url.pathname,
contentLength: request.headers.get("content-length"),
}); If you must log a body for debugging, use the redaction function:
if (process.env.LOG_BODIES === "true") {
logger.debug("request body", { body: redact(requestBody) });
} [!NOTE] The Web Security Fundamentals course covered sensitive data exposure. Logging is one of the most common sources of accidental exposure. Redaction is your safety net.
Exercises
Exercise 1: Build the redact function. Test with objects containing password, token, and apiKey fields.
Exercise 2: Integrate redaction into the Logger class. Log an object with sensitive fields. Verify they are redacted in the output.
Exercise 3: Add a mask function for partial visibility. Test with API keys and card numbers.
Why redact sensitive data automatically in the logger instead of relying on developers to exclude it?