package support; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; /** * MOAD-0004: A Logged Secret. * * Defect: HTTP request headers are serialized to a log string verbatim. * Authorization, Cookie, X-API-Key, and Proxy-Authorization headers contain * bearer tokens and session credentials. Once logged, these persist across * restarts, propagate through log aggregation pipelines, and remain readable * for weeks at retention-period scale. * * Fix: apply a credential denylist at the log-serialization layer. * Redact or omit headers whose names match known credential carriers. * Functional behavior (routing, auth checks) is unaffected. * * Scanner detects: header-map serialization (toString, stream map, for-each * with string append) without a denylist filter on known credential header names. */ public class Moad0004Algorithm { /** Headers that carry credentials and must never appear in logs. */ public static final Set CREDENTIAL_HEADERS = Set.of( "authorization", "cookie", "x-api-key", "proxy-authorization", "x-auth-token" ); /** Result of a log operation: the string written and a count of leaked credentials. */ public static final class Result { public final String logLine; public final int credentialLeakCount; public Result(String logLine, int credentialLeakCount) { this.logLine = logLine; this.credentialLeakCount = credentialLeakCount; } } // ── Defective: logs all headers verbatim ───────────────────────────────── /** * Serializes the full header map to a log line with no filtering. * DEFECT: credential headers appear in cleartext in the log string. */ public static Result logDefective(Map headers) { StringBuilder sb = new StringBuilder("REQUEST headers: "); int leaks = 0; for (Map.Entry e : headers.entrySet()) { sb.append(e.getKey()).append('=').append(e.getValue()).append(' '); if (CREDENTIAL_HEADERS.contains(e.getKey().toLowerCase())) { leaks++; } } return new Result(sb.toString().trim(), leaks); } // ── Fixed: redacts credential headers before serialization ──────────────── private static final String REDACTED = "[REDACTED]"; /** * Serializes the header map, replacing credential header values with [REDACTED]. * FIX: credential headers appear in the log line with redacted values. * Downstream consumers still see header names (for debugging) but not values. */ public static Result logFixed(Map headers) { StringBuilder sb = new StringBuilder("REQUEST headers: "); for (Map.Entry e : headers.entrySet()) { String value = CREDENTIAL_HEADERS.contains(e.getKey().toLowerCase()) ? REDACTED : e.getValue(); sb.append(e.getKey()).append('=').append(value).append(' '); } return new Result(sb.toString().trim(), 0); } /** Build a representative HTTP request header map for testing. */ public static Map sampleHeaders() { Map h = new LinkedHashMap<>(); h.put("Host", "api.example.com"); h.put("Content-Type", "application/json"); h.put("Authorization", "Bearer eyJhbGciOiJSUzI1NiJ9.secret.token"); h.put("Cookie", "session=abc123def456; csrf=xyz"); h.put("X-Request-Id", "req-7f3a9c"); h.put("X-Api-Key", "sk-prod-1234567890abcdef"); h.put("Accept", "application/json"); return h; } }