4.25 Review Sensitive Logging
4.25 - Review Sensitive Logging
Logs are a secondary data store. Review log statements, structured logging fields, exception serializers, and APM exporters for passwords, tokens, and payment data. Confirm security-relevant events are captured while secrets and regulated personal data are excluded or redacted.
What This Vulnerability Is
Sensitive logging is the practice of writing data to logs that should remain confidential. If logs are copied to analytics, emailed on alert, or accessed by operators with broad read rights, a single log.info("password=" + password) can cause a breach as serious as database exposure.
The unsafe assumption is that logs are internal and low risk. In practice, log platforms aggregate production traffic, retain data for months, and sync to third parties. This maps to CWE-532 (Insertion of Sensitive Information into Log File).
Vulnerability Characteristics (Where to Identify Them)
| Signal | Where to look |
|---|---|
| Feature type | Login, payment, password reset, OAuth token exchange, API key validation, crypto operations |
| Log sinks | Application loggers, access logs, APM traces, exception handlers, stdout in containers |
| Sensitive fields | Passwords, session IDs, bearer tokens, API keys, PAN/CVV, reset tokens, connection strings |
| Weak controls | Interpolating request.json, full header dumps, debug logging on auth paths in production |
| Missing positives | No audit trail for login failure, lockout, or admin actions without storing secrets |
| Shipping risk | Log aggregation to Splunk, CloudWatch, or ELK without redaction filters |
Attack Payloads
These are abuse scenarios for what attackers or insiders may recover from logs—not payloads to send to the app. Use them to design log review checklists and redaction tests.
Pattern 1: Credential capture in application logs
INFO login attempt user=admin password=Secret123!
DEBUG auth body={"password":"x","token":"eyJ..."}
Pattern 2: Token and session leakage
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Set-Cookie: session=deadbeef; Path=/
API key validated: sk_live_abc123xyz
Pattern 3: Payment and regulated data
card=4111111111111111 cvv=123 exp=12/29
ssn=123-45-6789
Pattern 4: Log injection via user-controlled fields (secondary risk)
username=admin%0aINFO Forged audit: admin logged in
Newline or forged severity in usernames may confuse parsers or SIEM rules.
Pattern 5: Exception and stack trace disclosure
SQLException: connection failed for user 'dbadmin' password 'DbP@ss!' at jdbc:mysql://internal-db:3306/prod
Pattern 6: Full request dumps in debug mode
REQUEST_HEADERS={... Authorization: Bearer ... Cookie: session=...}
REQUEST_BODY={"password":"..."}
Language-Specific Sinks and Dangerous APIs
Search for log calls and serializers that include request objects, headers, or exception messages with user data.
Python
logger.info("login %s %s", user, password)
logger.debug("headers %s body %s", request.headers, request.get_data())
app.logger.exception(e) # may include SQL with secrets
print(request.json) in containers; structlog with unfiltered request dict.
Java
log.info("token={}", accessToken);
log.debug("request {}", request.toString());
e.printStackTrace(); // stderr captured by log agents
Log4j/SLF4J MDC with full Authorization header; Spring CommonsRequestLoggingFilter without masking.
C
_logger.LogInformation("Password {Pwd}", password);
_logger.LogDebug("Request {@Request}", request);
Serilog destructuring of entire request DTOs; ILogger with connection strings in messages.
JavaScript
console.log("auth", req.headers.authorization, req.body);
logger.info({ headers: req.headers, body: req.body });
Winston/Pino serializers that pass through req unchanged.
Go
log.Printf("login user=%s pass=%s", user, pass)
log.Printf("req=%+v", r) // may dump Authorization header
Access and infrastructure logs
# nginx — full URI with secrets if clients use GET login
GET /login?password=secret HTTP/1.1
Sample Vulnerable Code in Python
import structlog
from fastapi import FastAPI, Request
app = FastAPI()
log = structlog.get_logger()
@app.post("/oauth/token")
async def token_exchange(request: Request):
body = await request.json()
# Sink: refresh token and client secret written to structured logs
log.debug("token_exchange", grant=body)
return await issue_tokens(body)
Step-by-Step Review Walkthrough
- Search log calls with request data. Look for interpolation of parameters, headers, cookies, or full JSON bodies.
- Trace the OAuth token exchange handler. In the sample, the full grant body may contain refresh tokens and client secrets. Debug level does not make the data safe in production.
- Review exception handlers. Messages that echo SQL or user input can land in log files and APM.
- Inspect access log configuration. Query strings and
Authorizationheaders must not appear on auth endpoints. - Check reset and MFA flows. Tokens or OTP codes left in stdout or logs from development leftovers are common findings.
- Review aggregation filters. Redaction must run before ship to external log platforms.
- Confirm audit events record outcome without credentials. Log actor, action, and result—not secrets.
Risk Impact Analysis
Credential and token exposure. Operators, support staff, and compromised log accounts may read secrets written to shared platforms.
PCI and regulatory breach. Cardholder data in logs can expand PCI scope and trigger notification obligations.
Long retention windows. Log retention often exceeds database row lifetimes. A single mistake persists for months.
Supply chain to third parties. Log SaaS vendors and analytics pipelines receive copies of whatever the application emits.
Vulnerable Examples in Other Languages
Java
public void login(String username, String password) {
logger.info("Login attempt user={} password={}", username, password);
boolean ok = authService.authenticate(username, password);
logger.info("Login result user={} success={}", username, ok);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleError(Exception ex, HttpServletRequest req) {
logger.error("request failed uri={} query={} body={}",
req.getRequestURI(), req.getQueryString(), readBody(req), ex);
return ResponseEntity.status(500).body(ex.getMessage());
}
C
[HttpPost("pay")]
public IActionResult Pay([FromBody] PaymentRequest req)
{
_logger.LogDebug("charge payload {@Card}", req.Card);
return Ok(_billing.Charge(req));
}
[HttpGet("admin/sync")]
public IActionResult Sync()
{
_logger.LogInformation("API call Authorization: {Auth}",
Request.Headers["Authorization"]);
return Ok(_sync.Run());
}
Go
func reset(w http.ResponseWriter, r *http.Request) {
token := generateToken()
log.Printf("issued reset token=%s for %s", token, r.FormValue("email"))
mailer.SendReset(r.FormValue("email"), token)
}
func pay(w http.ResponseWriter, r *http.Request) {
var payload map[string]interface{}
json.NewDecoder(r.Body).Decode(&payload)
log.Printf("charge request=%+v", payload) // may include PAN and CVV
processPayment(payload)
}
Fix: Safer Patterns and Libraries to Use
Python
Log event metadata, not secrets. Use filters to redact sensitive keys before emit.
import logging
import re
SENSITIVE_KEYS = re.compile(r"(password|token|secret|authorization|cvv|pan)", re.I)
class RedactFilter(logging.Filter):
def filter(self, record):
if isinstance(record.msg, str):
record.msg = SENSITIVE_KEYS.sub("[REDACTED]", record.msg)
return True
@app.post("/pay")
def pay():
payload = request.json
current_app.logger.info(
"charge_attempt user_id=%s amount=%s result=pending",
session.get("user_id"),
payload.get("amount"),
)
return charge(payload)
# Register filter on the app logger
logging.getLogger("werkzeug").addFilter(RedactFilter())
Important: Never log request.data, request.form, or full headers on authentication routes.
Java
Use structured logging with fixed fields. Never pass raw passwords to the logger.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
private static final Logger log = LoggerFactory.getLogger(AuthService.class);
public void login(String username, String password) {
MDC.put("event", "LOGIN_ATTEMPT");
MDC.put("username", username);
boolean ok = authenticate(username, password);
log.info("login result={}", ok ? "SUCCESS" : "FAILURE");
MDC.clear();
}
C
Use explicit log templates. Do not pass raw header dictionaries.
_logger.LogInformation(
"Login attempt for user {UserId} result {Result}",
userId,
success ? "Success" : "Failure");
// Serilog destructuring policy example
Log.Logger = new LoggerConfiguration()
.Destructure.ByTransforming<LoginRequest>(r => new { r.Username })
.CreateLogger();
Go
Use slog with an allowlist of attributes. Implement LogValuer for types that redact secrets.
import "log/slog"
func handleLogin(w http.ResponseWriter, r *http.Request) {
user := r.FormValue("user")
ok := authenticate(user, r.FormValue("pass"))
slog.Info("login_attempt",
slog.String("user", user),
slog.String("result", map[bool]string{true: "success", false: "failure"}[ok]),
)
}
type redactedString string
func (s redactedString) LogValue() slog.Value {
return slog.StringValue("[REDACTED]")
}
Verify During Review
- Passwords, session IDs, access tokens, API keys, and connection strings never appear in application or access logs.
- Security-relevant events (login, logout, reset attempts, authz failures, admin actions) are logged with useful non-secret context.
- Debug logging of full requests is disabled in production or heavily redacted.
- Log shipping and retention policies treat log buckets as sensitive data stores.
- Developers removed temporary debug prints of tokens from reset and OAuth flows.
- Exception messages returned to users are separate from rich detail allowed only in server-side logs.