import java.util.*; /** * pgbouncer-0001: CWE-312 — SCRAM verifier logged verbatim at debug level * * Source: src/client.c, function scram_client_first() * Line: slog_debug(client, "stored secret = \"%s\"", user->passwd); * * Defect: During SCRAM-SHA-256 authentication, PgBouncer logs the user's stored * secret (user->passwd) at slog_debug level. Depending on the auth_type configured, * user->passwd may contain: * - A plaintext password (auth_type=plain) * - A SCRAM-SHA-256 verifier: "SCRAM-SHA-256$:$:" * * Even the SCRAM verifier is sensitive: it can be used in an offline dictionary attack * or, for plaintext passwords, directly as the credential. Logging it to a debug log * file violates CWE-312 (Cleartext Storage of Sensitive Information). * * Fix: Remove the slog_debug line. The log event type ("SCRAM client-first received") * is still present on line 1119; no diagnostic value is lost. * * CVE-applicable: Yes — debug log file exposure of authentication credential. * Severity: MEDIUM-HIGH (requires debug log access, but many ops enable debug logging * during troubleshooting, leaving credential in log files indefinitely). */ public class PgBouncerScramSecretLogTest { // Simulate the log capture system static List capturedLogs = new ArrayList<>(); static void slog_debug(String context, String fmt, Object... args) { capturedLogs.add(String.format("[DEBUG][" + context + "] " + fmt, args)); } // Simulated PgCredentials static class PgCredentials { String name; String passwd; boolean mock_auth; PgCredentials(String name, String passwd) { this.name = name; this.passwd = passwd; this.mock_auth = false; } } // --- DEFECTIVE: logs stored secret --- static void scram_client_first_defective(String clientContext, PgCredentials user, String clientFirstMessage) { slog_debug(clientContext, "SCRAM client-first-message = \"%s\"", clientFirstMessage); if (!user.mock_auth) { // DEFECT: logs the SCRAM verifier or plaintext password slog_debug(clientContext, "stored secret = \"%s\"", user.passwd); } // ... rest of SCRAM processing } // --- FIXED: does not log stored secret --- static void scram_client_first_fixed(String clientContext, PgCredentials user, String clientFirstMessage) { slog_debug(clientContext, "SCRAM client-first-message = \"%s\"", clientFirstMessage); if (!user.mock_auth) { // Fixed: no credential logging. Comment in code: // /* Do not log user->passwd: it holds the SCRAM verifier or plaintext password (CWE-312). */ } // ... rest of SCRAM processing } static void testDefectiveLogsSecret() { capturedLogs.clear(); PgCredentials user = new PgCredentials("alice", "SCRAM-SHA-256$4096:c2FsdHNhbHRzYWx0$StoredKeyHere:ServerKeyHere"); scram_client_first_defective("client:127.0.0.1:5432", user, "n,,n=alice,r=clientnonce"); boolean secretFound = capturedLogs.stream().anyMatch(line -> line.contains("stored secret")); assert secretFound : "Defective impl should log 'stored secret'"; System.out.println("PASS defective: secret IS logged: " + capturedLogs.stream().filter(l -> l.contains("stored secret")).findFirst().orElse("?")); } static void testFixedDoesNotLogSecret() { capturedLogs.clear(); PgCredentials user = new PgCredentials("alice", "SCRAM-SHA-256$4096:c2FsdHNhbHRzYWx0$StoredKeyHere:ServerKeyHere"); scram_client_first_fixed("client:127.0.0.1:5432", user, "n,,n=alice,r=clientnonce"); boolean secretFound = capturedLogs.stream().anyMatch(line -> line.contains("stored secret")); assert !secretFound : "Fixed impl must NOT log 'stored secret', got: " + capturedLogs; System.out.println("PASS fixed: secret is NOT logged (logs: " + capturedLogs.size() + " line(s))"); } static void testFixedStillLogsDiagnostic() { capturedLogs.clear(); PgCredentials user = new PgCredentials("bob", "SCRAM-SHA-256$4096:abc$defgh:ijklm"); scram_client_first_fixed("client:127.0.0.1:5433", user, "n,,n=bob,r=bobnonce"); boolean clientFirstLogged = capturedLogs.stream().anyMatch(line -> line.contains("client-first-message")); assert clientFirstLogged : "Fixed impl must still log SCRAM client-first-message event"; System.out.println("PASS fixed: diagnostic 'client-first-message' still logged"); } static void testPlaintextPasswordNotLogged() { capturedLogs.clear(); PgCredentials user = new PgCredentials("carol", "supersecretpassword123"); scram_client_first_fixed("client:192.168.1.1:5432", user, "n,,n=carol,r=carolnonce"); boolean plainPassFound = capturedLogs.stream().anyMatch(line -> line.contains("supersecretpassword123")); assert !plainPassFound : "Fixed impl must NOT log plaintext passwords"; System.out.println("PASS fixed: plaintext password NOT logged"); } static void testMockAuthNotLogged() { capturedLogs.clear(); PgCredentials user = new PgCredentials("mockuser", "not-a-real-secret"); user.mock_auth = true; // Both defective and fixed skip the branch when mock_auth=true scram_client_first_defective("client:10.0.0.1:5432", user, "n,,n=mockuser,r=mocknonce"); boolean secretFound = capturedLogs.stream().anyMatch(line -> line.contains("stored secret")); assert !secretFound : "mock_auth=true: secret branch should be skipped"; System.out.println("PASS mock-auth: secret branch skipped for mock_auth=true"); } public static void main(String[] args) { System.out.println("=== pgbouncer-0001: CWE-312 SCRAM verifier logged at debug ==="); testDefectiveLogsSecret(); testFixedDoesNotLogSecret(); testFixedStillLogsDiagnostic(); testPlaintextPasswordNotLogged(); testMockAuthNotLogged(); System.out.println("ALL PASS"); } }