ffmpeg-0004 (MOAD-0004 / CWE-312): libavformat/http.c http_connect() logs the full HTTP request at AV_LOG_DEBUG, including the Authorization: Basic header with base64-encoded user:pass. Fix: produce a sanitized copy before av_log, replacing auth header values with ***REDACTED***. Wire bytes unchanged. 22/22 unit tests PASS. FFmpeg MOADs 0002/0003/0005: CLEAN. ImageMagick MOADs 0002-0005: CLEAN.
218 lines
9.6 KiB
Java
218 lines
9.6 KiB
Java
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 <b64>" 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);
|
|
}
|
|
}
|