java-topology/defects/thunderbird-0008/test/ThunderbirdOAuth2TokenLogTest.java

145 lines
7.3 KiB
Java

import java.util.*;
/**
* Unit test for thunderbird-0008: OAuth2.sys.mjs access_token and refresh_token
* logged verbatim in successful and error response paths (CWE-312).
*
* Models the JavaScript pattern:
* const resultStr = JSON.stringify(result);
* log.info(`Successful response from the authorization server: ${resultStr}`);
*
* A typical successful OAuth2 response:
* { "access_token": "ya29.XYZ...", "refresh_token": "1//0g...", "expires_in": 3600 }
*
* resultStr is logged with full token values. When mailnews.oauth.loglevel is
* set to "Info" (common during user/developer troubleshooting), tokens appear
* in the Thunderbird error console and application log.
*
* Fix: shallow-copy result, replace sensitive fields with "[redacted]" before stringify.
*/
public class ThunderbirdOAuth2TokenLogTest {
// --- Simulate defective logging ---
static String buildLogMessageDefective(Map<String, String> oauthResponse) {
// Mirrors: const resultStr = JSON.stringify(result);
// log.info(`Successful response from the authorization server: ${resultStr}`);
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, String> e : oauthResponse.entrySet()) {
if (!first) sb.append(", ");
sb.append("\"").append(e.getKey()).append("\": \"").append(e.getValue()).append("\"");
first = false;
}
sb.append("}");
return "Successful response from the authorization server: " + sb;
}
// --- Simulate fixed logging ---
static String buildLogMessageFixed(Map<String, String> oauthResponse) {
// Mirrors the patch: redact sensitive fields before stringify
Map<String, String> safe = new LinkedHashMap<>(oauthResponse);
for (String field : new String[]{"access_token", "refresh_token", "id_token"}) {
if (safe.containsKey(field)) {
safe.put(field, "[redacted]");
}
}
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, String> e : safe.entrySet()) {
if (!first) sb.append(", ");
sb.append("\"").append(e.getKey()).append("\": \"").append(e.getValue()).append("\"");
first = false;
}
sb.append("}");
return "Successful response from the authorization server: " + sb;
}
public static void main(String[] args) {
System.out.println("=== thunderbird-0008: OAuth2 token logging CWE-312 ===\n");
// Simulate a real OAuth2 successful response
Map<String, String> oauthResponse = new LinkedHashMap<>();
oauthResponse.put("access_token", "ya29.A0ARrdaM9_SENSITIVE_ACCESS_TOKEN_XYZ");
oauthResponse.put("refresh_token", "1//0gSENSITIVE_REFRESH_TOKEN_ABC");
oauthResponse.put("token_type", "Bearer");
oauthResponse.put("expires_in", "3600");
oauthResponse.put("scope", "https://mail.google.com/");
// Test 1: defective path leaks tokens in log
{
String logMsg = buildLogMessageDefective(oauthResponse);
boolean leaksAccess = logMsg.contains("ya29.A0ARrdaM9_SENSITIVE_ACCESS_TOKEN_XYZ");
boolean leaksRefresh = logMsg.contains("1//0gSENSITIVE_REFRESH_TOKEN_ABC");
assert leaksAccess : "FAIL: expected defective path to contain access_token";
assert leaksRefresh : "FAIL: expected defective path to contain refresh_token";
System.out.println("CONFIRMED defective: access_token in log = " + leaksAccess);
System.out.println("CONFIRMED defective: refresh_token in log = " + leaksRefresh);
}
// Test 2: fixed path redacts tokens
{
String logMsg = buildLogMessageFixed(oauthResponse);
boolean hiddenAccess = !logMsg.contains("ya29.A0ARrdaM9_SENSITIVE_ACCESS_TOKEN_XYZ");
boolean hiddenRefresh = !logMsg.contains("1//0gSENSITIVE_REFRESH_TOKEN_ABC");
boolean hasRedacted = logMsg.contains("[redacted]");
assert hiddenAccess : "FAIL: fixed path should not contain access_token";
assert hiddenRefresh : "FAIL: fixed path should not contain refresh_token";
assert hasRedacted : "FAIL: fixed path should contain [redacted] marker";
System.out.println("PASS fixed: access_token hidden = " + hiddenAccess);
System.out.println("PASS fixed: refresh_token hidden = " + hiddenRefresh);
System.out.println("PASS fixed: [redacted] present = " + hasRedacted);
}
// Test 3: fixed path preserves non-sensitive fields (useful diagnostic info)
{
String logMsg = buildLogMessageFixed(oauthResponse);
assert logMsg.contains("Bearer") : "FAIL: token_type should still be logged";
assert logMsg.contains("3600") : "FAIL: expires_in should still be logged";
assert logMsg.contains("mail.google.com") : "FAIL: scope should still be logged";
System.out.println("PASS: non-sensitive fields preserved in fixed log message");
}
// Test 4: response without refresh_token (PKCE flow) — only access_token redacted
{
Map<String, String> pkceResponse = new LinkedHashMap<>();
pkceResponse.put("access_token", "ya29.PKCE_ACCESS_TOKEN");
pkceResponse.put("token_type", "Bearer");
pkceResponse.put("expires_in", "3600");
String logMsg = buildLogMessageFixed(pkceResponse);
assert !logMsg.contains("ya29.PKCE_ACCESS_TOKEN") : "FAIL: PKCE access_token should be redacted";
assert logMsg.contains("[redacted]") : "FAIL: [redacted] marker expected";
System.out.println("PASS: PKCE (no refresh_token) access_token redacted correctly");
}
// Test 5: id_token (OpenID Connect) also redacted
{
Map<String, String> oidcResponse = new LinkedHashMap<>();
oidcResponse.put("access_token", "AT_SENSITIVE");
oidcResponse.put("id_token", "eyJhbGciOiJSU0I_SENSITIVE_ID_TOKEN");
oidcResponse.put("token_type", "Bearer");
String logMsg = buildLogMessageFixed(oidcResponse);
assert !logMsg.contains("eyJhbGciOiJSU0I_SENSITIVE_ID_TOKEN") : "FAIL: id_token should be redacted";
System.out.println("PASS: id_token (OpenID Connect) also redacted");
}
// Test 6: error response — ensure resultStr does not leak any partial token
// Error case at line 333: log.info(`Error response details: ${resultStr}`)
// Some providers return token data in error responses too (e.g. invalid_scope with partial grant)
{
Map<String, String> errorResponse = new LinkedHashMap<>();
errorResponse.put("error", "invalid_scope");
errorResponse.put("access_token", "ya29.PARTIAL_TOKEN_ON_ERROR");
errorResponse.put("error_description", "Requested scope not allowed");
String logMsg = buildLogMessageFixed(errorResponse);
assert !logMsg.contains("ya29.PARTIAL_TOKEN_ON_ERROR") : "FAIL: partial token on error should be redacted";
System.out.println("PASS: error response partial token also redacted");
}
System.out.println("\nAll tests PASS");
}
}