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 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 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 oauthResponse) { // Mirrors the patch: redact sensitive fields before stringify Map 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 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 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 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 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 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"); } }