From d9d4b30defa7f8853276c5bc7320f2c6d9ba979b Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 21:37:39 -0400 Subject: [PATCH] evolution+pidgin: 5-MOAD scan; 3 defects (evolution-0001 CWE-407, pidgin-0001 CWE-407, pidgin-0002 CWE-312) --- defects/evolution-0001/SCAN-NOTES.md | 58 +++++++++ .../evolution-0001-exdate-dedup-hashset.patch | 43 +++++++ .../unit/EDateTimeListTest.java | 112 ++++++++++++++++++ defects/pidgin-0001/SCAN-NOTES.md | 64 ++++++++++ .../pidgin-0001-permit-list-hashset.patch | 38 ++++++ .../pidgin-0001/unit/PidginPrivacyTest.java | 105 ++++++++++++++++ defects/pidgin-0002/SCAN-NOTES.md | 55 +++++++++ .../patch/pidgin-0002-cred-logged.md | 77 ++++++++++++ 8 files changed, 552 insertions(+) create mode 100644 defects/evolution-0001/SCAN-NOTES.md create mode 100644 defects/evolution-0001/patch/evolution-0001-exdate-dedup-hashset.patch create mode 100644 defects/evolution-0001/unit/EDateTimeListTest.java create mode 100644 defects/pidgin-0001/SCAN-NOTES.md create mode 100644 defects/pidgin-0001/patch/pidgin-0001-permit-list-hashset.patch create mode 100644 defects/pidgin-0001/unit/PidginPrivacyTest.java create mode 100644 defects/pidgin-0002/SCAN-NOTES.md create mode 100644 defects/pidgin-0002/patch/pidgin-0002-cred-logged.md diff --git a/defects/evolution-0001/SCAN-NOTES.md b/defects/evolution-0001/SCAN-NOTES.md new file mode 100644 index 000000000..c2f8df2ca --- /dev/null +++ b/defects/evolution-0001/SCAN-NOTES.md @@ -0,0 +1,58 @@ +# evolution-0001: EXDATE dedup O(E^2) in EDateTimeList + +## Target +GNOME Evolution email/calendar client + +## Severity +LOW-MEDIUM + +## MOAD +0001 (CWE-407: Inefficient Algorithmic Complexity) + +## Location +`src/calendar/gui/e-date-time-list.c` — `e_date_time_list_append()` +Called from `src/calendar/gui/e-comp-editor-page-recurrence.c` — `ecep_recurrence_fill_exception_widgets()` + +## Description +`e_date_time_list_append()` deduplicates exception dates (EXDATE properties +on a recurring calendar event) using `g_list_find_custom()` on a GList. + +When loading a recurring calendar event with E exception dates, +`ecep_recurrence_fill_exception_widgets()` calls append E times in a for loop. +Each call scans up to E-1 entries already stored: O(1) + O(2) + ... + O(E-1) += O(E^2/2) total comparisons. + +Real events can accumulate hundreds of exception dates over years (e.g., a +weekly recurring meeting with individual cancellations). + +## Fix +Maintain a parallel `GHashTable` in `EDateTimeListPrivate` keyed on the +ISO date string (via `i_cal_time_as_ical_string()`). Membership test in +`e_date_time_list_append()` becomes O(1). Clear the hash table in +`e_date_time_list_clear()`. + +## Speedup +| E | Ops (defective) | Ops (fixed) | Ratio | +|---|-----------------|-------------|-------| +| 50 | 1225 | 50 | 24.5x | +| 100 | 4950 | 100 | 49.5x | +| 200 | 19900 | 200 | 99.5x | +| 500 | 124750 | 500 | 249.5x | +| 1000 | 499500 | 1000 | 499.5x | + +## MOAD-0002 (Intertangle) +`src/mail/mail-send-recv.c`: static globals `send_data`, `send_recv_dialog`, +`glob_ongoing_downsyncs` are shared mutable state across mail send/receive +operations and UI callbacks. No explicit mutex guards these. Architectural +MOAD-0002, no single-function patch. + +## MOAD-0003 +CLEAN. No ThreadLocal or GPrivate request-scoped identity leakage found. + +## MOAD-0004 +CLEAN. No credential values (passwords, tokens) logged verbatim. Debug +messages reference password operations by description only. + +## MOAD-0005 +CLEAN. Evolution uses GLib main loop (single UI thread). No concurrent +cache-get+put race conditions. diff --git a/defects/evolution-0001/patch/evolution-0001-exdate-dedup-hashset.patch b/defects/evolution-0001/patch/evolution-0001-exdate-dedup-hashset.patch new file mode 100644 index 000000000..bdd407cb1 --- /dev/null +++ b/defects/evolution-0001/patch/evolution-0001-exdate-dedup-hashset.patch @@ -0,0 +1,43 @@ +# UNDF: +--- a/src/calendar/gui/e-date-time-list.c ++++ b/src/calendar/gui/e-date-time-list.c +@@ -56,6 +56,7 @@ struct _EDateTimeListPrivate { + GList *list; + gboolean use_24_hour_format; + gint stamp; ++ GHashTable *date_set; /* ISO-date string keys for O(1) dedup */ + }; + + /* EDateTimeList is a GtkTreeModel implementation that stores ICalTime +@@ -580,8 +580,21 @@ e_date_time_list_append (EDateTimeList *date_time_list, + g_return_if_fail (i_cal_time_is_valid_time ((ICalTime *) itt)); + +- if (g_list_find_custom (date_time_list->priv->list, itt, (GCompareFunc) compare_datetime) == NULL) { ++ if (date_time_list->priv->date_set == NULL) ++ date_time_list->priv->date_set = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); ++ ++ /* Build a canonical key: YYYYMMDD or YYYYMMDDTHHMMSS[Z] */ ++ gchar *key = i_cal_time_as_ical_string ((ICalTime *) itt); ++ if (key == NULL) ++ return; ++ ++ if (!g_hash_table_contains (date_time_list->priv->date_set, key)) { ++ g_hash_table_add (date_time_list->priv->date_set, key); /* takes ownership */ + date_time_list->priv->list = g_list_append (date_time_list->priv->list, i_cal_time_clone (itt)); + row_added (date_time_list, g_list_length (date_time_list->priv->list) - 1); ++ } else { ++ g_free (key); + } + + if (iter) { +@@ -598,6 +611,10 @@ e_date_time_list_clear (EDateTimeList *date_time_list) + g_return_if_fail (E_IS_DATE_TIME_LIST (date_time_list)); + ++ if (date_time_list->priv->date_set) { ++ g_hash_table_destroy (date_time_list->priv->date_set); ++ date_time_list->priv->date_set = NULL; ++ } ++ + g_list_free_full (date_time_list->priv->list, g_object_unref); + date_time_list->priv->list = NULL; + date_time_list->priv->stamp++; diff --git a/defects/evolution-0001/unit/EDateTimeListTest.java b/defects/evolution-0001/unit/EDateTimeListTest.java new file mode 100644 index 000000000..5065de65c --- /dev/null +++ b/defects/evolution-0001/unit/EDateTimeListTest.java @@ -0,0 +1,112 @@ +import java.util.*; + +/** + * Models evolution-0001: GNOME Evolution e-date-time-list.c + * + * e_date_time_list_append() deduplicates exception dates (EXDATE) using + * g_list_find_custom() on a GList. When loading a recurring calendar event + * with E exception dates, the fill loop calls append E times, each scanning + * up to E-1 entries already in the list: O(E^2) comparisons total. + * + * Fix: maintain a parallel GHashTable keyed on ISO date string for O(1) + * membership test. Append stays O(1) amortized; fill loop is O(E) total. + */ +public class EDateTimeListTest { + + // --- Defective model: GList linear scan on each append --- + static int appendDefective(List list, String date) { + for (String existing : list) { // O(N) scan + if (existing.equals(date)) return 0; // duplicate found + } + list.add(date); + return 1; + } + + static long fillExceptionsDefective(List dates) { + List store = new ArrayList<>(); + long ops = 0; + for (String d : dates) { + ops += store.size(); // scan cost + appendDefective(store, d); + } + return ops; + } + + // --- Fixed model: HashSet for O(1) membership --- + static long fillExceptionsFixed(List dates) { + Set seen = new HashSet<>(); + List store = new ArrayList<>(); + long ops = 0; + for (String d : dates) { + ops += 1; // O(1) hash lookup + if (seen.add(d)) store.add(d); + } + return ops; + } + + static List generateDates(int n) { + List dates = new ArrayList<>(); + for (int i = 0; i < n; i++) { + // simulate EXDATE strings like 20260101, 20260102, ... + int day = i % 28 + 1; + int month = (i / 28) % 12 + 1; + int year = 2026 + (i / 336); + dates.add(String.format("%04d%02d%02d", year, month, day)); + } + return dates; + } + + public static void main(String[] args) { + System.out.println("evolution-0001: e-date-time-list EXDATE dedup O(E^2) vs O(E)"); + System.out.println("============================================================="); + + int[] sizes = {50, 100, 200, 500, 1000}; + + for (int n : sizes) { + List dates = generateDates(n); + + long tDefStart = System.nanoTime(); + long opsDefective = fillExceptionsDefective(new ArrayList<>(dates)); + long tDefEnd = System.nanoTime(); + + long tFixStart = System.nanoTime(); + long opsFixed = fillExceptionsFixed(new ArrayList<>(dates)); + long tFixEnd = System.nanoTime(); + + long timeDefective = tDefEnd - tDefStart; + long timeFixed = tFixEnd - tFixStart; + double ratio = (double) opsDefective / opsFixed; + + System.out.printf("E=%4d: defective=%6d ops (%6.3f ms) | fixed=%6d ops (%6.3f ms) | ratio=%.1fx%n", + n, + opsDefective, timeDefective / 1_000_000.0, + opsFixed, timeFixed / 1_000_000.0, + ratio); + } + + // Correctness: verify both produce same unique list + List testDates = new ArrayList<>(); + testDates.add("20260101"); + testDates.add("20260102"); + testDates.add("20260101"); // duplicate + testDates.add("20260103"); + + List defStore = new ArrayList<>(); + for (String d : testDates) appendDefective(defStore, d); + + Set fixSeen = new HashSet<>(); + List fixStore = new ArrayList<>(); + for (String d : testDates) { if (fixSeen.add(d)) fixStore.add(d); } + + assert defStore.equals(fixStore) : "Mismatch: " + defStore + " vs " + fixStore; + System.out.println("\nCorrectness: PASS (both produce same deduplicated list)"); + + // Verify ratio at E=1000 + List bigDates = generateDates(1000); + long bigDef = fillExceptionsDefective(new ArrayList<>(bigDates)); + long bigFix = fillExceptionsFixed(new ArrayList<>(bigDates)); + double bigRatio = (double) bigDef / bigFix; + assert bigRatio > 200 : "Expected >200x ratio at E=1000, got " + bigRatio; + System.out.printf("Speedup at E=1000: %.0fx -- PASS%n", bigRatio); + } +} diff --git a/defects/pidgin-0001/SCAN-NOTES.md b/defects/pidgin-0001/SCAN-NOTES.md new file mode 100644 index 000000000..b02744613 --- /dev/null +++ b/defects/pidgin-0001/SCAN-NOTES.md @@ -0,0 +1,64 @@ +# pidgin-0001: add_all_buddies_to_permit_list O(B^2) GSList scan + +## Target +Pidgin 2.14.x (libpurple) + +## Severity +MEDIUM + +## MOAD +0001 (CWE-407: Inefficient Algorithmic Complexity) + +## Location +`libpurple/privacy.c` — `add_all_buddies_to_permit_list()` + +Called from: +- `purple_privacy_allow()` when switching from `ALLOW_BUDDYLIST` mode +- `purple_privacy_deny()` when switching from `ALLOW_BUDDYLIST` mode + +## Description +`add_all_buddies_to_permit_list()` synchronizes our account's permit (allow) +list with our buddy list. It calls `purple_find_buddies(account, NULL)` to +get all B buddies, then iterates them. For each buddy it calls: + +```c +g_slist_find_custom(account->permit, name, (GCompareFunc)g_utf8_collate) +``` + +`account->permit` is a GSList. As buddies are added to our permit list, our +list grows to size B. Each subsequent `g_slist_find_custom` scans more of our +growing list. Total comparisons: 0 + 1 + 2 + ... + (B-1) = O(B^2/2). + +With a large buddy list (e.g. 1000 contacts), switching privacy modes triggers +499,500 string comparisons instead of 1,000. + +## Fix +Before our buddy iteration loop, snapshot `account->permit` into a +`GHashTable` for O(1) membership tests. Replace `g_slist_find_custom()` +with `g_hash_table_lookup()`. Destroy our snapshot hash table after our loop. + +## Speedup +| B (buddies) | Defective ops | Fixed ops | Ratio | +|-------------|---------------|-----------|-------| +| 100 | 4,950 | 100 | 49.5x | +| 500 | 124,750 | 500 | 249.5x | +| 1,000 | 499,500 | 1,000 | 499.5x | +| 2,000 | 1,999,000 | 2,000 | 999.5x | + +## Also noted in blist.c +`purple_group_get_accounts()` and `purple_blist_remove_account()` use the +same O(N^2) dedup-accumulate pattern with `g_slist_find` / `g_list_find`. +These are lower severity (accounts per group is typically 1-5) but share +our root cause. + +## MOAD-0002 (Intertangle) +`libpurple/blist.c`: `purplebuddylist`, `buddies_cache`, `groups_cache` are +global mutable singletons accessed by all protocol callbacks. No mutex. +Architectural intertangle; not a single-function patch. + +## MOAD-0003 +CLEAN. Pidgin is C/GLib main-loop single-threaded. No ThreadLocal or +GPrivate request-scoped identity leakage found. + +## MOAD-0005 +CLEAN. Single-threaded GLib event loop; no concurrent cache-get+put races. diff --git a/defects/pidgin-0001/patch/pidgin-0001-permit-list-hashset.patch b/defects/pidgin-0001/patch/pidgin-0001-permit-list-hashset.patch new file mode 100644 index 000000000..60d4b7036 --- /dev/null +++ b/defects/pidgin-0001/patch/pidgin-0001-permit-list-hashset.patch @@ -0,0 +1,38 @@ +# UNDF: +--- a/libpurple/privacy.c ++++ b/libpurple/privacy.c +@@ -231,18 +231,24 @@ static void + add_all_buddies_to_permit_list(PurpleAccount *account, gboolean local) + { + GSList *list; ++ GHashTable *permit_set; ++ GSList *permit_iter; + + /* Remove anyone in the permit list who is not in the buddylist */ + for (list = account->permit; list != NULL; ) { + char *person = list->data; + list = list->next; + if (!purple_find_buddy(account, person)) + purple_privacy_permit_remove(account, person, local); + } + ++ /* Build a hash set of existing permit names for O(1) membership test */ ++ permit_set = g_hash_table_new(g_str_hash, g_str_equal); ++ for (permit_iter = account->permit; permit_iter != NULL; permit_iter = permit_iter->next) ++ g_hash_table_add(permit_set, permit_iter->data); ++ + /* Now make sure everyone in the buddylist is in the permit list */ + list = purple_find_buddies(account, NULL); + while (list != NULL) + { + PurpleBuddy *buddy = list->data; + const gchar *name = purple_buddy_get_name(buddy); + +- if (!g_slist_find_custom(account->permit, name, (GCompareFunc)g_utf8_collate)) ++ if (!g_hash_table_lookup(permit_set, name)) + purple_privacy_permit_add(account, name, local); + list = g_slist_delete_link(list, list); + } ++ ++ g_hash_table_destroy(permit_set); + } diff --git a/defects/pidgin-0001/unit/PidginPrivacyTest.java b/defects/pidgin-0001/unit/PidginPrivacyTest.java new file mode 100644 index 000000000..ea03a6648 --- /dev/null +++ b/defects/pidgin-0001/unit/PidginPrivacyTest.java @@ -0,0 +1,105 @@ +import java.util.*; + +/** + * Models pidgin-0001: libpurple/privacy.c + * + * add_all_buddies_to_permit_list() ensures every buddy on our buddy list is + * also on the permit (allow) list. It iterates all B buddies and for each + * calls g_slist_find_custom(account->permit, name, g_utf8_collate), scanning + * our permit GSList linearly: O(B * P) where P = permit list size (grows to B). + * Total: O(B^2) for B buddies all on our ALLOW_BUDDYLIST privacy mode. + * + * Fix: before our buddy loop, snapshot account->permit into a GHashTable for + * O(1) membership tests. Our while loop becomes O(B) total. + */ +public class PidginPrivacyTest { + + // --- Defective model: GSList linear scan per buddy --- + static long addAllBuddiesDefective(List buddies) { + List permit = new ArrayList<>(); + long ops = 0; + for (String buddy : buddies) { + // simulate g_slist_find_custom scan of permit list + boolean found = false; + for (String p : permit) { // O(P) + ops++; + if (p.equals(buddy)) { found = true; break; } + } + if (!found) permit.add(buddy); + } + return ops; + } + + // --- Fixed model: GHashTable snapshot for O(1) lookup --- + static long addAllBuddiesFixed(List buddies) { + Set permitSet = new HashSet<>(); // snapshot of existing permit + List permit = new ArrayList<>(); + long ops = 0; + for (String buddy : buddies) { + ops++; // O(1) hash lookup + if (!permitSet.contains(buddy)) { + permitSet.add(buddy); + permit.add(buddy); + } + } + return ops; + } + + static List generateBuddies(int n) { + List buddies = new ArrayList<>(); + for (int i = 0; i < n; i++) + buddies.add("buddy" + i + "@example.com"); + return buddies; + } + + public static void main(String[] args) { + System.out.println("pidgin-0001: add_all_buddies_to_permit_list O(B^2) vs O(B)"); + System.out.println("============================================================"); + + int[] sizes = {50, 100, 200, 500, 1000, 2000}; + + for (int n : sizes) { + List buddies = generateBuddies(n); + + long tDefStart = System.nanoTime(); + long opsDefective = addAllBuddiesDefective(new ArrayList<>(buddies)); + long tDefEnd = System.nanoTime(); + + long tFixStart = System.nanoTime(); + long opsFixed = addAllBuddiesFixed(new ArrayList<>(buddies)); + long tFixEnd = System.nanoTime(); + + long timeDefective = tDefEnd - tDefStart; + long timeFixed = tFixEnd - tFixStart; + double ratio = (double) opsDefective / opsFixed; + + System.out.printf("B=%4d: defective=%8d ops (%6.3f ms) | fixed=%6d ops (%6.3f ms) | ratio=%.1fx%n", + n, + opsDefective, timeDefective / 1_000_000.0, + opsFixed, timeFixed / 1_000_000.0, + ratio); + } + + // Correctness: both models produce same permit list + List test = Arrays.asList("alice@x.com", "bob@x.com", "alice@x.com", "carol@x.com"); + + Set defSeen = new LinkedHashSet<>(); + for (String b : test) defSeen.add(b); + + Set fixSeen = new HashSet<>(); + List fixPermit = new ArrayList<>(); + for (String b : test) { if (fixSeen.add(b)) fixPermit.add(b); } + + assert new ArrayList<>(defSeen).equals(fixPermit) + : "Mismatch: " + defSeen + " vs " + fixPermit; + System.out.println("\nCorrectness: PASS"); + + // Verify ratio at B=1000 + List bigBuddies = generateBuddies(1000); + long bigDef = addAllBuddiesDefective(new ArrayList<>(bigBuddies)); + long bigFix = addAllBuddiesFixed(new ArrayList<>(bigBuddies)); + double bigRatio = (double) bigDef / bigFix; + assert bigRatio > 200 : "Expected >200x at B=1000, got " + bigRatio; + System.out.printf("Speedup at B=1000: %.0fx -- PASS%n", bigRatio); + } +} diff --git a/defects/pidgin-0002/SCAN-NOTES.md b/defects/pidgin-0002/SCAN-NOTES.md new file mode 100644 index 000000000..e5ff7a0df --- /dev/null +++ b/defects/pidgin-0002/SCAN-NOTES.md @@ -0,0 +1,55 @@ +# pidgin-0002: SIP SIMPLE Authorization header logged verbatim (CWE-312) + +## Target +Pidgin 2.14.x (libpurple SIMPLE/SIP protocol plugin) + +## Severity +MEDIUM-HIGH + +## MOAD +0004 (CWE-312: Cleartext Storage of Sensitive Information) + +## Location +`libpurple/protocols/simple/simple.c` lines 664 and 669 + +## Description +Our SIMPLE (SIP) protocol plugin constructs `Authorization` and +`Proxy-Authorization` headers via `auth_header()` and immediately logs our +full header value to our debug output: + +```c +purple_debug(PURPLE_DEBUG_MISC, "simple", "header %s", auth); +``` + +Our `auth` variable contains one of: +- **Digest response**: `Digest username="...", realm="...", nonce="...", response=""` — our response + encodes a hash derived from our account password and can be used in a replay attack + or subjected to offline dictionary attack. +- **NTLM Type3 blob**: our gssapi-data field is a full NTLM challenge-response + produced by `purple_ntlm_gen_type3(authuser, sip->password, ...)`. Our + NTLM hash is directly crackable offline with hashcat mode 5600 (NetNTLMv2). + +Pidgin debug output goes to: +1. Our Debug Window (visible to shoulder-surfers) +2. Our console if started with debug flags +3. Crash dumps / bug report data +4. Any log file a user has configured + +## Fix +Remove our `purple_debug()` calls at lines 664 and 669, or replace with +a redacted version that logs only our method and auth type. + +## MOAD-0001 (CWE-407) +See `pidgin-0001` for our primary CWE-407 defect in `privacy.c`. +Our `simple.c` also has: `fill_auth()` at line 45 loops over received +`WWW-Authenticate` header list; no O(N^2) pattern, each auth candidate +is tested once. CLEAN for MOAD-0001 in our simple.c itself. + +## MOAD-0002 (Intertangle) +Documented in `pidgin-0001/SCAN-NOTES.md`. + +## MOAD-0003 +CLEAN. + +## MOAD-0005 +CLEAN. diff --git a/defects/pidgin-0002/patch/pidgin-0002-cred-logged.md b/defects/pidgin-0002/patch/pidgin-0002-cred-logged.md new file mode 100644 index 000000000..b58cc35b9 --- /dev/null +++ b/defects/pidgin-0002/patch/pidgin-0002-cred-logged.md @@ -0,0 +1,77 @@ +# pidgin-0002: SIP SIMPLE Authorization header logged verbatim (CWE-312) + +## MOAD +0004 (CWE-312: Cleartext Storage of Sensitive Information) + +## Severity +MEDIUM-HIGH + +## Location +`libpurple/protocols/simple/simple.c` lines 661-669 + +## Exact code + +```c +/* line 661-664 */ +if(sip->registrar.type && purple_strequal(method, "REGISTER")) { + buf = auth_header(sip, &sip->registrar, method, url); + auth = g_strdup_printf("Authorization: %s\r\n", buf); + g_free(buf); + purple_debug(PURPLE_DEBUG_MISC, "simple", "header %s", auth); +/* line 666-669 */ +} else if(sip->proxy.type && !purple_strequal(method, "REGISTER")) { + buf = auth_header(sip, &sip->proxy, method, url); + auth = g_strdup_printf("Proxy-Authorization: %s\r\n", buf); + g_free(buf); + purple_debug(PURPLE_DEBUG_MISC, "simple", "header %s", auth); +} +``` + +## What auth_header() returns + +`auth_header()` at line 264 returns a full SIP authorization credential: + +**Digest** (line 287/308): +``` +Digest username="alice", realm="corp.example.com", nonce="abc123", uri="sip:corp.example.com", nc="00000001", response="a3f2c1b9..." +``` +Our `response` field is `H(H(username:realm:password):nonce:H(method:uri))`. +Logging it enables offline Digest authentication replay attacks. + +**NTLM** (line 293-294): +``` +NTLM qop="auth", opaque="...", realm="corp.example.com", targetname="CORP", gssapi-data="" +``` +Our NTLM Type3 blob is produced by `purple_ntlm_gen_type3(authuser, sip->password, ...)`. +It encodes our NTLM hash derived directly from our account password. +An attacker who reads our Pidgin debug log obtains an NTLM hash crackable offline. + +## Credential type +- Digest: session-specific response hash (replay risk, offline preimage attack risk) +- NTLM: password-derived challenge response (offline crack risk with hashcat/john) + +## Affected users +Anyone using Pidgin with our SIMPLE/SIP protocol plugin who has debug logging +enabled (`purple_debug_is_enabled()` returns TRUE, which is on by default +when Pidgin is built with `--enable-debug` or when Debug Window is open). + +## Fix + +Remove or redact our authorization header from debug output: + +```c +/* Replace the two purple_debug() lines */ + +/* Option A: remove logging entirely */ +/* (delete the purple_debug() calls) */ + +/* Option B: log method+URI only, strip credential value */ +purple_debug(PURPLE_DEBUG_MISC, "simple", + "sending auth header type=%d for method=%s\n", + sip->registrar.type, method); +``` + +## References +- CWE-312: https://cwe.mitre.org/data/definitions/312.html +- NTLM hash cracking: `hashcat -m 5600` NetNTLMv2 +- Pidgin SIMPLE plugin: `libpurple/protocols/simple/simple.c`