import java.util.*; import java.util.regex.*; /** * CWE-312 simulation: FFmpeg libavformat/http.c — Authorization header logged at AV_LOG_DEBUG * * http_connect() builds the full HTTP request and logs it verbatim at AV_LOG_DEBUG: * * av_log(h, AV_LOG_DEBUG, "request: %s\n", request.str); * * The request includes the Authorization header assembled by ff_http_auth_create_response(): * * Authorization: Basic dXNlcjpwYXNz\r\n * * base64("user:pass") = "dXNlcjpwYXNz" — decoded trivially. * * AV_LOG_DEBUG is active for any user running ffmpeg -loglevel debug or in * production services with verbose logging enabled. Credentials appear in * terminal output, log files, and aggregation services. * * Fix: before calling av_log, produce a sanitized copy of the request string * where the value of Authorization and Proxy-Authorization headers is replaced * with "***REDACTED***". Wire bytes are unchanged; only the log line is safe. * * This test verifies: * 1. The vulnerable path: credentials appear in the raw request string. * 2. The patched path: credentials are redacted in the logged string. * 3. The wire bytes (un-sanitized) remain correct after patching. * 4. Non-auth headers are preserved unchanged. * 5. Digest auth (multi-field) is also redacted. */ public class FFmpegHttpAuthLogTest { // --- Simulate the defect: verbatim request logging --- /** Build a simulated HTTP request string with Basic auth. */ static String buildRequestWithBasicAuth(String user, String pass) { String credentials = Base64.getEncoder() .encodeToString((user + ":" + pass).getBytes()); return "GET /stream HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: Lavf/61.0\r\n" + "Accept: */*\r\n" + "Authorization: Basic " + credentials + "\r\n" + "Connection: close\r\n" + "\r\n"; } /** Build a simulated HTTP request string with Digest auth. */ static String buildRequestWithDigestAuth(String digestValue) { return "GET /stream HTTP/1.1\r\n" + "Host: example.com\r\n" + "Authorization: Digest " + digestValue + "\r\n" + "Connection: close\r\n" + "\r\n"; } /** Build a request with Proxy-Authorization as well. */ static String buildRequestWithProxyAuth(String user, String pass) { String creds = Base64.getEncoder() .encodeToString((user + ":" + pass).getBytes()); return "CONNECT example.com:443 HTTP/1.1\r\n" + "Host: example.com:443\r\n" + "Proxy-Authorization: Basic " + creds + "\r\n" + "Connection: keep-alive\r\n" + "\r\n"; } // --- Simulate the fix: redact auth headers before logging --- static final String[] AUTH_HEADER_PREFIXES = { "Authorization: ", "Proxy-Authorization: " }; /** * Produces a sanitized copy of the HTTP request string. * Auth header values are replaced with "***REDACTED***". * Header names and all other content are preserved. */ static String sanitizeForLogging(String request) { String result = request; for (String prefix : AUTH_HEADER_PREFIXES) { // Case-insensitive match for the header name Pattern pat = Pattern.compile( "(?i)(" + Pattern.quote(prefix) + ")([^\r\n]+)"); Matcher m = pat.matcher(result); result = m.replaceAll("$1***REDACTED***"); } return result; } /** Decode base64 credential from "Authorization: Basic " header value. */ static String decodeBasicCred(String request) { Pattern p = Pattern.compile("Authorization: Basic ([A-Za-z0-9+/=]+)"); Matcher m = p.matcher(request); if (!m.find()) return null; return new String(Base64.getDecoder().decode(m.group(1))); } // --- Test cases --- static int pass = 0, fail = 0; static void check(String description, boolean condition) { if (condition) { System.out.println(" PASS: " + description); pass++; } else { System.out.println(" FAIL: " + description); fail++; } } public static void main(String[] args) { System.out.println("FFmpegHttpAuthLogTest — CWE-312 Authorization header log redaction"); System.out.println("================================================================="); // --- Test 1: Defect — credentials visible in raw request --- System.out.println("\nTest 1: Defect — Basic auth credentials present in raw request"); { String req = buildRequestWithBasicAuth("alice", "s3cr3t!"); String decoded = decodeBasicCred(req); check("Raw request contains base64-encoded credentials", req.contains("Authorization: Basic ")); check("Decoded credential is 'alice:s3cr3t!'", "alice:s3cr3t!".equals(decoded)); check("Plaintext password recoverable from raw request", decoded != null && decoded.contains("s3cr3t!")); } // --- Test 2: Fix — sanitized log string has no credentials --- System.out.println("\nTest 2: Fix — sanitized log string redacts Basic auth value"); { String req = buildRequestWithBasicAuth("alice", "s3cr3t!"); String sanitized = sanitizeForLogging(req); check("Sanitized log contains 'Authorization: ' header name", sanitized.contains("Authorization: ")); check("Sanitized log contains REDACTED marker", sanitized.contains("***REDACTED***")); check("Sanitized log does NOT contain base64 credentials", !sanitized.contains("YWxpY2U6czNjcjN0IQ==")); // Verify base64 decode of the marker fails (not a credential) check("REDACTED marker is not valid base64 user:pass", !sanitized.matches(".*Authorization: Basic [A-Za-z0-9+/=]+.*")); } // --- Test 3: Wire bytes unchanged — fix does not alter actual transmission --- System.out.println("\nTest 3: Wire bytes unchanged after fix"); { String req = buildRequestWithBasicAuth("bob", "p@ssw0rd"); String sanitized = sanitizeForLogging(req); // The wire bytes (req) still carry the real credentials check("Wire request still contains actual Authorization header", req.contains("Authorization: Basic ")); check("Decoded wire credential is 'bob:p@ssw0rd'", "bob:p@ssw0rd".equals(decodeBasicCred(req))); // But the sanitized log string does not check("Log string does not contain real credential", decodeBasicCred(sanitized) == null); } // --- Test 4: Non-auth headers preserved --- System.out.println("\nTest 4: Non-auth headers preserved in sanitized log"); { String req = buildRequestWithBasicAuth("user", "pass"); String sanitized = sanitizeForLogging(req); check("Host header preserved", sanitized.contains("Host: example.com")); check("User-Agent header preserved", sanitized.contains("User-Agent: Lavf/61.0")); check("Accept header preserved", sanitized.contains("Accept: */*")); check("Connection header preserved", sanitized.contains("Connection: close")); } // --- Test 5: Digest auth also redacted --- System.out.println("\nTest 5: Digest auth value is also redacted"); { String digestValue = "realm=\"secure\", nonce=\"abc123\", " + "username=\"admin\", response=\"deadbeef\""; String req = buildRequestWithDigestAuth(digestValue); String sanitized = sanitizeForLogging(req); check("Raw Digest request contains username", req.contains("username=\"admin\"")); check("Sanitized Digest request does NOT contain username", !sanitized.contains("username=\"admin\"")); check("Sanitized Digest request contains REDACTED marker", sanitized.contains("***REDACTED***")); } // --- Test 6: Proxy-Authorization redacted --- System.out.println("\nTest 6: Proxy-Authorization header value is also redacted"); { String req = buildRequestWithProxyAuth("proxyuser", "proxypass"); String sanitized = sanitizeForLogging(req); check("Raw request contains Proxy-Authorization header", req.contains("Proxy-Authorization: ")); check("Sanitized log does NOT contain proxy credential", !sanitized.contains("proxyuser")); check("Sanitized log contains REDACTED marker for proxy cred", sanitized.contains("***REDACTED***")); check("Sanitized log preserves Connection header", sanitized.contains("Connection: keep-alive")); } // --- Test 7: Request with NO auth headers is unchanged by sanitizer --- System.out.println("\nTest 7: Request with no auth headers passes through unchanged"); { String req = "GET /public HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"; String sanitized = sanitizeForLogging(req); check("Non-auth request is identical after sanitization", req.equals(sanitized)); } // --- Summary --- System.out.println("\n================================================================="); System.out.println("Results: " + pass + " PASS, " + fail + " FAIL"); if (fail > 0) System.exit(1); } }