From 62305ef3394d48ad1ed0b6eb905149484fbaf0e3 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 20:02:17 -0400 Subject: [PATCH] =?UTF-8?q?thunderbird:=202=20defects=20(0007=20O(N=C2=B2)?= =?UTF-8?q?=20folder=20tree=20init,=200008=20CWE-312=20OAuth2=20token=20lo?= =?UTF-8?q?gging);=20MOADs=200002/0003/0005=20CLEAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...out3Pane_initServer_existingURIs_ON2.patch | 49 ++++++ .../test/ThunderbirdInitServerTest.class | Bin 0 -> 4298 bytes .../test/ThunderbirdInitServerTest.java | 146 ++++++++++++++++++ ...008_OAuth2_accessToken_logged_CWE312.patch | 38 +++++ .../test/ThunderbirdOAuth2TokenLogTest.class | Bin 0 -> 5375 bytes .../test/ThunderbirdOAuth2TokenLogTest.java | 145 +++++++++++++++++ 6 files changed, 378 insertions(+) create mode 100644 defects/thunderbird-0007/patch/thunderbird-0007_about3Pane_initServer_existingURIs_ON2.patch create mode 100644 defects/thunderbird-0007/test/ThunderbirdInitServerTest.class create mode 100644 defects/thunderbird-0007/test/ThunderbirdInitServerTest.java create mode 100644 defects/thunderbird-0008/patch/thunderbird-0008_OAuth2_accessToken_logged_CWE312.patch create mode 100644 defects/thunderbird-0008/test/ThunderbirdOAuth2TokenLogTest.class create mode 100644 defects/thunderbird-0008/test/ThunderbirdOAuth2TokenLogTest.java diff --git a/defects/thunderbird-0007/patch/thunderbird-0007_about3Pane_initServer_existingURIs_ON2.patch b/defects/thunderbird-0007/patch/thunderbird-0007_about3Pane_initServer_existingURIs_ON2.patch new file mode 100644 index 000000000..df2958196 --- /dev/null +++ b/defects/thunderbird-0007/patch/thunderbird-0007_about3Pane_initServer_existingURIs_ON2.patch @@ -0,0 +1,49 @@ +--- a/mail/base/content/about3Pane.js ++++ b/mail/base/content/about3Pane.js +@@ -1241,20 +1241,21 @@ MOAD-0001 CWE-407: about3Pane.js SmartServerPane.initServer() O(N²) folder dedup + # Severity: MEDIUM-HIGH + # Ratio: O(F²) where F = number of folders in a server (e.g. 500 IMAP folders → 250,000 ops) + # + # Root cause: initServer() builds existingURIs as an Array, then calls existingURIs.includes() + # inside a do-while loop over remainingFolderURIs. After each addFolder() call the array is + # fully rebuilt from DOM (Array.from(existingRows, li => li.uri)). Every includes() call is + # O(F) and there are F iterations → O(F²) total. + # + # Fix: use a Set for existingURIs. Seed it once from DOM; when addFolder() adds a row + # update the Set directly instead of re-scanning the entire live NodeList. + + initServer(server) { + // Find all folders in this server, and display the ones that aren't + // currently displayed. + const descendants = new Map( + server.rootFolder.descendants.map(d => [d.URI, d]) + ); + if (!descendants.size) { + return; + } + const remainingFolderURIs = Array.from(descendants.keys()); + + // Get a list of folders that already exist in the folder tree. + const existingRows = this.containerList.getElementsByTagName("li"); +- let existingURIs = Array.from(existingRows, li => li.uri); ++ // PATCH thunderbird-0007: use a Set so membership checks are O(1). ++ const existingURIs = new Set(Array.from(existingRows, li => li.uri)); + do { + const folderURI = remainingFolderURIs.shift(); +- if (existingURIs.includes(folderURI)) { ++ if (existingURIs.has(folderURI)) { + continue; + } + const folder = descendants.get(folderURI); + if (folderPane._isGmailFolder(folder)) { + continue; + } + this.addFolder(folderPane._getNonGmailParent(folder), folder); +- // Update the list of existing folders. `existingRows` is a live +- // list, so we don't need to call `getElementsByTagName` again. +- existingURIs = Array.from(existingRows, li => li.uri); ++ // Update the Set so subsequent iterations see the newly added row. ++ // `existingRows` is a live NodeList — we just add the new URI. ++ existingURIs.add(folderURI); + } while (remainingFolderURIs.length); + }, diff --git a/defects/thunderbird-0007/test/ThunderbirdInitServerTest.class b/defects/thunderbird-0007/test/ThunderbirdInitServerTest.class new file mode 100644 index 0000000000000000000000000000000000000000..c97f7d6d4e6d1712a8b65a3641632924d5acbadf GIT binary patch literal 4298 zcma)9TXbAi8UFU%&dKz&Nrz6Fv;+>&(4?I-0ot_9r6jbCosvr-r8N{Rr zfA9bQ{{Q>`z2{H=z5FhKe!LS$2w?>g4N=4d>P{Q8Mtjzvc!HTo8>u{V|TY6TZ6S&r=V6t z9o7r1D~ri91QaWOFgN4P`^xT?(pPENfF!MO&73o93M5<7t-W%IWtqkwL3M8;wUw2G4grSm3fgrVC9Wd zSu-Ob?pAP2puuO4&+q<{)IryETos=cxMrX~J=mpZN`~pK=@net%;+7uGn0?wF5E3M z@x%(J2MNtARB^Asrr?wbY1oPr5)e2kNqV0^eM@>Z=BYR(5Hj0T(CU!Oc0yqHaBp+l z&Wv6*t+%-|BW==kUr%o{SN)-D5P5I&j+-Xtn(a8IU@17QVH(*|a{1`fwr5V7E_pO- zWDDj9mC&Hd{4Yv$$6>=!FeA_Sd{rtOa0+y;^u>Y7jlnec?Ll6HhXQMiPgd&aStb8Y zRFJFW$zZCQ^JLilw0FX1CJEhvc?}QX3#>r4VLL~yTu?|3tN0={HQNJMI&Dw2(@H*c zX4t(c zif;&Xu4Hk3$|+*d`H1|H9RZtmfCU-?awmT$efIsw_G!`oKmBn;T5J@V z<|C^(CB~XRFx~#3Xna@|0 zvtA)XpDL2qAE@}HKx;r4b8d!vWX+N#Bw&`|=>xsHIyzGNq~qyS@hje;z6v9oZChqg zwUVGBiP{)mkK+Q~Q1ENX>Nf?pl&NFcv(B{H9(Xgjs|Sp6&vE%gd9Jcb6a@o695U=o z*354ibe!qJOpyeBeig$~L6xeFN9SkERm3Z!S6zGjLyJf3C)iQTGrI+}v~8QN&wDek zV3D`u6_{2tRl#M3{r`)yP6oJe9q+Rci-M?Yke3>JG2R?}NAmMIj+)pCw$W`DAzqh) zc)lnEB%sQ>mIp}DzcH;VjBUS&nxXJM+2zifu88cVWG~tnxdct%1srUQgfAfx z!tYSu6?-f!8l#P|OK1q8Q%Ne%BX)l2FDW^5bqL4H`Gu^-Z-z#Fw-D-1LhU5f-L!l!L3MG|&0Y@<;a1$i;GW<&gwNqNAL8|^L);4d z2Q7s75l<>3=z`SsdBStiI`~un&yWYa?SfY0k zw+^a_gKUOU+sU2J_}r-`NGS@7IPB9;lCC^A+!?umJ6^#0rn6Z4Htrfr9KVRM-}-Zj zWTZ5CPcYful#E=a`ykc76CVrL#Ye^>bvKWUMe8yngWKQjr6;aFhljT}?dXg|69z5o ziY6n8aoLGUwT-c&{>CUfG^$fsZF;y{=H@se{kC+?qNNy2lXn%dYURfSW zi+w+DZqPPh;n}lD$N)5Gm+?4Iba>)Py1Fjo!x$`uZAf;iJ)FH484_Tg{OyDvm@n@Eg4{#d) z#I&f#X|aj#Xq^w&EpSB&d2tKhqkAwbhWMDg6LVq`^CHVfWC0I~NBM9(%l0KaB;Mo` z@DFU?$D`syoDqM=0+Zsap)ei~ZNw9y6rK$A;i=Fto(>(uGkzu?L2QZUL>0VPELJSi z8>l1x%;ae|2iJV6y z{08nQJ5AmX@&b^#97Ra$;}HUV+2U1a5W H8;$=1Xe&Ce literal 0 HcmV?d00001 diff --git a/defects/thunderbird-0007/test/ThunderbirdInitServerTest.java b/defects/thunderbird-0007/test/ThunderbirdInitServerTest.java new file mode 100644 index 000000000..b757dbffa --- /dev/null +++ b/defects/thunderbird-0007/test/ThunderbirdInitServerTest.java @@ -0,0 +1,146 @@ +import java.util.*; + +/** + * Unit test for thunderbird-0007: about3Pane.js SmartServerPane.initServer() + * existingURIs Array.includes() O(N²) in do-while loop. + * + * Models the JavaScript pattern: + * let existingURIs = Array.from(existingRows, li => li.uri); // re-built each iteration + * do { + * const folderURI = remainingFolderURIs.shift(); + * if (existingURIs.includes(folderURI)) continue; // O(F) per check + * this.addFolder(...); + * existingURIs = Array.from(existingRows, li => li.uri); // full rebuild! + * } while (remainingFolderURIs.length); + * + * Fix: replace Array with Set so membership checks are O(1). + */ +public class ThunderbirdInitServerTest { + + // --- Defective implementation (Array + linear includes + full rebuild) --- + + static int initServerDefective(List remainingFolderURIs, + List initialExistingURIs) { + List existingRows = new ArrayList<>(initialExistingURIs); + int ops = 0; + + List remaining = new ArrayList<>(remainingFolderURIs); + List existingURIs = new ArrayList<>(existingRows); + + while (!remaining.isEmpty()) { + String folderURI = remaining.remove(0); + // O(F) scan — the defect + ops++; + boolean found = existingURIs.contains(folderURI); + if (found) continue; + + // addFolder: mutates existingRows + existingRows.add(folderURI); + + // Full rebuild — another O(F) scan hidden here + existingURIs = new ArrayList<>(existingRows); + ops += existingRows.size(); // cost of rebuild + } + return ops; + } + + // --- Fixed implementation (Set + O(1) has()) --- + + static int initServerFixed(List remainingFolderURIs, + List initialExistingURIs) { + List existingRows = new ArrayList<>(initialExistingURIs); + int ops = 0; + + List remaining = new ArrayList<>(remainingFolderURIs); + Set existingURIs = new HashSet<>(existingRows); + + while (!remaining.isEmpty()) { + String folderURI = remaining.remove(0); + ops++; + if (existingURIs.contains(folderURI)) continue; + + // addFolder: just add to Set, no rebuild + existingRows.add(folderURI); + existingURIs.add(folderURI); + } + return ops; + } + + // --- Helper --- + + static List makeFolderURIs(int count, String prefix) { + List uris = new ArrayList<>(); + for (int i = 0; i < count; i++) { + uris.add("imap://user@server/mailbox/" + prefix + i); + } + return uris; + } + + public static void main(String[] args) { + System.out.println("=== thunderbird-0007: initServer existingURIs O(N²) ===\n"); + + // Test 1: correctness - no duplicates processed + { + List existing = makeFolderURIs(3, "existing-"); + List remaining = makeFolderURIs(5, "new-"); + // Add overlap: one of the new folders is already in existing + remaining.add(existing.get(0)); // duplicate + + // Both implementations should accept the same non-duplicate count + // (just verify they run without error and give non-negative ops) + int opsD = initServerDefective(new ArrayList<>(remaining), new ArrayList<>(existing)); + int opsF = initServerFixed(new ArrayList<>(remaining), new ArrayList<>(existing)); + assert opsD > 0 : "FAIL: defective returned 0 ops"; + assert opsF > 0 : "FAIL: fixed returned 0 ops"; + System.out.println("PASS correctness: defective=" + opsD + " ops, fixed=" + opsF + " ops"); + } + + // Test 2: small N — verify both give same logical result + { + for (int n : new int[]{10, 50, 100}) { + List existing = makeFolderURIs(2, "e-"); + List remaining = makeFolderURIs(n, "r-"); + int opsD = initServerDefective(new ArrayList<>(remaining), new ArrayList<>(existing)); + int opsF = initServerFixed(new ArrayList<>(remaining), new ArrayList<>(existing)); + // Fixed should always use fewer ops than defective for N > small constant + System.out.printf("N=%-4d defective=%6d ops fixed=%4d ops ratio=%.1fx%n", + n, opsD, opsF, (double) opsD / opsF); + } + } + + // Test 3: ratio benchmark at F=500 (large IMAP account) + { + int F = 500; + List existing = makeFolderURIs(10, "existing-"); + List remaining = makeFolderURIs(F, "folder-"); + + long t0 = System.nanoTime(); + int opsD = initServerDefective(new ArrayList<>(remaining), new ArrayList<>(existing)); + long t1 = System.nanoTime(); + int opsF = initServerFixed(new ArrayList<>(remaining), new ArrayList<>(existing)); + long t2 = System.nanoTime(); + + double ratio = (double) opsD / opsF; + System.out.printf("%nF=%-4d defective=%8d ops fixed=%6d ops op-ratio=%.1fx%n", + F, opsD, opsF, ratio); + System.out.printf(" defective=%6d µs fixed=%5d µs%n", + (t1 - t0) / 1000, (t2 - t1) / 1000); + + assert ratio > 5.0 : "FAIL: expected ratio > 5x at F=500, got " + ratio; + System.out.println("PASS: ratio > 5x confirmed"); + } + + // Test 4: all-duplicate case (everything already in tree) + { + List existing = makeFolderURIs(100, "folder-"); + List remaining = new ArrayList<>(existing); // 100% overlap + int opsD = initServerDefective(new ArrayList<>(remaining), new ArrayList<>(existing)); + int opsF = initServerFixed(new ArrayList<>(remaining), new ArrayList<>(existing)); + // Both should still handle this correctly (skip all) + assert opsD > 0 : "FAIL: defective should still do ops on duplicate check"; + System.out.println("PASS all-duplicate: defective=" + opsD + " fixed=" + opsF); + } + + System.out.println("\nAll tests PASS"); + } +} diff --git a/defects/thunderbird-0008/patch/thunderbird-0008_OAuth2_accessToken_logged_CWE312.patch b/defects/thunderbird-0008/patch/thunderbird-0008_OAuth2_accessToken_logged_CWE312.patch new file mode 100644 index 000000000..8d9539dd8 --- /dev/null +++ b/defects/thunderbird-0008/patch/thunderbird-0008_OAuth2_accessToken_logged_CWE312.patch @@ -0,0 +1,38 @@ +--- a/mailnews/base/src/OAuth2.sys.mjs ++++ b/mailnews/base/src/OAuth2.sys.mjs +@@ -318,16 +318,25 @@ MOAD-0004 CWE-312: OAuth2.sys.mjs access_token and refresh_token logged verbatim + # Severity: HIGH + # CVE class: CWE-312 Cleartext Storage of Sensitive Information + # + # Root cause: requestAccessToken() receives the full JSON response from the OAuth2 + # authorization server, serializes it with JSON.stringify(result) → resultStr, then + # logs it verbatim at log.info level: + # + # line 333: log.info(`Error response details: ${resultStr}`) + # line 358: log.info(`Successful response from the authorization server: ${resultStr}`) + # + # A successful OAuth2 response always contains "access_token" and frequently + # "refresh_token". When a user or developer sets mailnews.oauth.loglevel to + # "All", "Debug", or "Info" (common during troubleshooting), these tokens are + # written to the application log and/or browser console. + # + # access_token grants full mailbox access (Gmail, Microsoft 365, Fastmail). + # refresh_token is long-lived and survives account password changes. + # Logging either constitutes CWE-312: credentials at rest in plaintext log files. + # + # Fix: strip sensitive fields before logging. Redact access_token, refresh_token, + # and id_token from the result object before calling JSON.stringify. + + .then(result => { +- const resultStr = JSON.stringify(result); ++ // PATCH thunderbird-0008: redact credential fields before logging (CWE-312). ++ const safeResult = Object.assign({}, result); ++ for (const field of ["access_token", "refresh_token", "id_token"]) { ++ if (field in safeResult) { ++ safeResult[field] = "[redacted]"; ++ } ++ } ++ const resultStr = JSON.stringify(safeResult); + if ("error" in result) { + // RFC 6749 section 5.2. Error Response + let err = result.error; diff --git a/defects/thunderbird-0008/test/ThunderbirdOAuth2TokenLogTest.class b/defects/thunderbird-0008/test/ThunderbirdOAuth2TokenLogTest.class new file mode 100644 index 0000000000000000000000000000000000000000..2a4a1a20e797e7afbf0c1cab4a986bc559fa16cf GIT binary patch literal 5375 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 literal 0 HcmV?d00001 diff --git a/defects/thunderbird-0008/test/ThunderbirdOAuth2TokenLogTest.java b/defects/thunderbird-0008/test/ThunderbirdOAuth2TokenLogTest.java new file mode 100644 index 000000000..d2cb358be --- /dev/null +++ b/defects/thunderbird-0008/test/ThunderbirdOAuth2TokenLogTest.java @@ -0,0 +1,145 @@ +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"); + } +}