145 lines
7.3 KiB
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");
|
|
}
|
|
}
|