evolution+pidgin: 5-MOAD scan; 3 defects (evolution-0001 CWE-407, pidgin-0001 CWE-407, pidgin-0002 CWE-312)
This commit is contained in:
parent
a512770591
commit
d9d4b30def
8 changed files with 552 additions and 0 deletions
58
defects/evolution-0001/SCAN-NOTES.md
Normal file
58
defects/evolution-0001/SCAN-NOTES.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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++;
|
||||
112
defects/evolution-0001/unit/EDateTimeListTest.java
Normal file
112
defects/evolution-0001/unit/EDateTimeListTest.java
Normal file
|
|
@ -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<String> 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<String> dates) {
|
||||
List<String> 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<String> dates) {
|
||||
Set<String> seen = new HashSet<>();
|
||||
List<String> 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<String> generateDates(int n) {
|
||||
List<String> 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<String> 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<String> testDates = new ArrayList<>();
|
||||
testDates.add("20260101");
|
||||
testDates.add("20260102");
|
||||
testDates.add("20260101"); // duplicate
|
||||
testDates.add("20260103");
|
||||
|
||||
List<String> defStore = new ArrayList<>();
|
||||
for (String d : testDates) appendDefective(defStore, d);
|
||||
|
||||
Set<String> fixSeen = new HashSet<>();
|
||||
List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
64
defects/pidgin-0001/SCAN-NOTES.md
Normal file
64
defects/pidgin-0001/SCAN-NOTES.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
105
defects/pidgin-0001/unit/PidginPrivacyTest.java
Normal file
105
defects/pidgin-0001/unit/PidginPrivacyTest.java
Normal file
|
|
@ -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<String> buddies) {
|
||||
List<String> 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<String> buddies) {
|
||||
Set<String> permitSet = new HashSet<>(); // snapshot of existing permit
|
||||
List<String> 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<String> generateBuddies(int n) {
|
||||
List<String> 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<String> 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<String> test = Arrays.asList("alice@x.com", "bob@x.com", "alice@x.com", "carol@x.com");
|
||||
|
||||
Set<String> defSeen = new LinkedHashSet<>();
|
||||
for (String b : test) defSeen.add(b);
|
||||
|
||||
Set<String> fixSeen = new HashSet<>();
|
||||
List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
55
defects/pidgin-0002/SCAN-NOTES.md
Normal file
55
defects/pidgin-0002/SCAN-NOTES.md
Normal file
|
|
@ -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="<HMAC>"` — 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.
|
||||
77
defects/pidgin-0002/patch/pidgin-0002-cred-logged.md
Normal file
77
defects/pidgin-0002/patch/pidgin-0002-cred-logged.md
Normal file
|
|
@ -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="<base64 NTLM Type3>"
|
||||
```
|
||||
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`
|
||||
Loading…
Add table
Add a link
Reference in a new issue