import java.util.*; /** * Unit tests for Dolibarr defects (CWE-407 and CWE-312). * * dolibarr-0001: filecheck.php in_array($tmprelativefilename, $file_list['insignature']) * — O(S*I) file membership check during integrity verification * dolibarr-0002: datapolicycron.class.php in_array($obj->rowid, $processedIds) * — O(N^2) dedup accumulator in GDPR data policy cron * dolibarr-0003: bookkeeping.class.php in_array($bookKeeping->piece_num, $alreadyExtourneT) * — O(P*E*A) reversal check in accounting journal extourne * dolibarr-0004: emailcollector_card.php dol_syslog(...password=$object->password...) * — CWE-312: IMAP password logged verbatim to system log * dolibarr-0005: functions_ldap.php dol_syslog(...Pass:dol_trunc($ldap->searchPassword, 3)) * — CWE-312: LDAP admin searchPassword first 3 chars logged (debug-gated but still leaks) */ public class DolibarrCWE407Test { // ---- dolibarr-0001: filecheck insignature membership ---- /** Unpatched: in_array on plain array — O(S*I) */ static int filecheckUnpatched(List scanFiles, List inSignature) { List added = new ArrayList<>(); for (String file : scanFiles) { if (!inSignature.contains(file)) { // O(I) per file added.add(file); } } return added.size(); } /** Patched: array_flip for O(1) lookup — O(S+I) */ static int filecheckPatched(List scanFiles, List inSignature) { Set sigSet = new HashSet<>(inSignature); // O(I) once List added = new ArrayList<>(); for (String file : scanFiles) { if (!sigSet.contains(file)) { // O(1) per file added.add(file); } } return added.size(); } // ---- dolibarr-0002: datapolicycron processedIds dedup ---- /** Unpatched: in_array on growing processedIds — O(N^2) */ static int datapolicyCronUnpatched(List dbRows) { List processedIds = new ArrayList<>(); int processed = 0; for (int rowid : dbRows) { if (!processedIds.contains(rowid)) { // O(N) scan processed++; processedIds.add(rowid); } } return processed; } /** Patched: array_flip for O(1) lookup — O(N) total */ static int datapolicyCronPatched(List dbRows) { Set processedSet = new HashSet<>(); int processed = 0; for (int rowid : dbRows) { if (!processedSet.contains(rowid)) { // O(1) lookup processed++; processedSet.add(rowid); } } return processed; } // ---- dolibarr-0003: bookkeeping extourne reversal check ---- /** Unpatched: in_array on alreadyExtourneT — O(P*E*A) */ static int extourneUnpatched(List pieceNums, List alreadyExtourne) { int reversed = 0; for (int pieceNum : pieceNums) { // Inner loop simulates fetching entries per piece for (int entry = 0; entry < 5; entry++) { if (alreadyExtourne.contains(pieceNum)) { // O(A) scan // already reversed — skip } else { reversed++; } } } return reversed; } /** Patched: hash set for O(1) lookup — O(P*E + A) */ static int extournePatched(List pieceNums, List alreadyExtourne) { Set alreadySet = new HashSet<>(alreadyExtourne); // O(A) once int reversed = 0; for (int pieceNum : pieceNums) { for (int entry = 0; entry < 5; entry++) { if (alreadySet.contains(pieceNum)) { // O(1) lookup // already reversed — skip } else { reversed++; } } } return reversed; } // ---- Test harness ---- static void testDolibarr0001() { System.out.println("=== dolibarr-0001: filecheck insignature membership ==="); int S = 16000; // scanned files (typical Dolibarr installation) int I = 14000; // files in signature List scanFiles = new ArrayList<>(S); List inSignature = new ArrayList<>(I); for (int i = 0; i < I; i++) { String name = "/htdocs/module" + (i / 100) + "/file" + i + ".php"; inSignature.add(name); scanFiles.add(name); } // Add some files not in signature for (int i = I; i < S; i++) { scanFiles.add("/htdocs/custom/file" + i + ".php"); } // Warmup filecheckPatched(scanFiles, inSignature); long t0 = System.nanoTime(); int r1 = filecheckUnpatched(scanFiles, inSignature); long unpatched = System.nanoTime() - t0; t0 = System.nanoTime(); int r2 = filecheckPatched(scanFiles, inSignature); long patched = System.nanoTime() - t0; assert r1 == r2 : "Result mismatch"; assert r1 == (S - I) : "Expected " + (S - I) + " added files, got " + r1; double ratio = (double) unpatched / patched; System.out.printf(" S=%d I=%d unpatched=%dms patched=%dms ratio=%.1fx%n", S, I, unpatched / 1_000_000, patched / 1_000_000, ratio); assert ratio > 5.0 : "Expected >5x ratio, got " + ratio; System.out.println(" PASS"); } static void testDolibarr0002() { System.out.println("=== dolibarr-0002: datapolicycron processedIds dedup ==="); int N = 10000; // GDPR records to process List dbRows = new ArrayList<>(N); for (int i = 0; i < N; i++) { dbRows.add(i); } // Add some duplicates for (int i = 0; i < N / 10; i++) { dbRows.add(i); } // Warmup datapolicyCronPatched(dbRows); long t0 = System.nanoTime(); int r1 = datapolicyCronUnpatched(dbRows); long unpatched = System.nanoTime() - t0; t0 = System.nanoTime(); int r2 = datapolicyCronPatched(dbRows); long patched = System.nanoTime() - t0; assert r1 == r2 : "Result mismatch"; assert r1 == N : "Expected " + N + " unique, got " + r1; double ratio = (double) unpatched / patched; System.out.printf(" N=%d unpatched=%dms patched=%dms ratio=%.1fx%n", N, unpatched / 1_000_000, patched / 1_000_000, ratio); assert ratio > 5.0 : "Expected >5x ratio, got " + ratio; System.out.println(" PASS"); } static void testDolibarr0003() { System.out.println("=== dolibarr-0003: bookkeeping extourne reversal check ==="); int P = 2000; // pieces to reverse int A = 5000; // already-reversed pieces List pieceNums = new ArrayList<>(P); for (int i = 0; i < P; i++) { pieceNums.add(A + i); // new pieces not in already-reversed set } List alreadyExtourne = new ArrayList<>(A); for (int i = 0; i < A; i++) { alreadyExtourne.add(i); } // Warmup extournePatched(pieceNums, alreadyExtourne); long t0 = System.nanoTime(); int r1 = extourneUnpatched(pieceNums, alreadyExtourne); long unpatched = System.nanoTime() - t0; t0 = System.nanoTime(); int r2 = extournePatched(pieceNums, alreadyExtourne); long patched = System.nanoTime() - t0; assert r1 == r2 : "Result mismatch"; assert r1 == P * 5 : "Expected " + (P * 5) + " reversed, got " + r1; double ratio = (double) unpatched / patched; System.out.printf(" P=%d A=%d unpatched=%dms patched=%dms ratio=%.1fx%n", P, A, unpatched / 1_000_000, patched / 1_000_000, ratio); assert ratio > 5.0 : "Expected >5x ratio, got " + ratio; System.out.println(" PASS"); } // ---- dolibarr-0004: IMAP password logged verbatim (CWE-312) ---- /** * Models the log message builder for emailcollector_card.php line 575. * Unpatched: concatenates $object->password verbatim into the log string. * Patched: replaces password with literal "***". */ static String imapLogUnpatched(String connectstring, String login, String password) { return "imap_open connectstring=" + connectstring + " login=" + login + " password=" + password; } static String imapLogPatched(String connectstring, String login, String password) { return "imap_open connectstring=" + connectstring + " login=" + login + " password=***"; } // ---- dolibarr-0005: LDAP searchPassword first 3 chars logged (CWE-312) ---- /** Simulates dol_trunc(password, 3): returns first 3 chars + "..." */ static String dolTrunc(String s, int n) { if (s == null || s.length() <= n) return s; return s.substring(0, n) + "..."; } static String ldapLogUnpatched(String searchUser, String searchPassword) { return "Admin:" + searchUser + ", Pass:" + dolTrunc(searchPassword, 3); } static String ldapLogPatched(String searchUser, String searchPassword) { return "Admin:" + searchUser + ", Pass:***"; } static void testDolibarr0004() { System.out.println("=== dolibarr-0004: IMAP password verbatim in log (CWE-312) ==="); String connectstring = "{imap.example.com:993/imap/ssl}INBOX"; String login = "user@example.com"; String password = "S3cr3tP@ss!"; String unpatched = imapLogUnpatched(connectstring, login, password); String patched = imapLogPatched(connectstring, login, password); // Unpatched log contains the real password assert unpatched.contains(password) : "Expected unpatched log to contain password"; // Patched log must NOT contain the real password assert !patched.contains(password) : "Patched log must not contain password"; assert patched.contains("***") : "Patched log must contain redaction marker"; // Both logs contain the login (not a secret) assert unpatched.contains(login) : "Log must contain login"; assert patched.contains(login) : "Log must contain login"; System.out.println(" unpatched log: " + unpatched); System.out.println(" patched log: " + patched); System.out.println(" PASS"); } static void testDolibarr0005() { System.out.println("=== dolibarr-0005: LDAP searchPassword partial leak in log (CWE-312) ==="); String searchUser = "cn=ldapadmin,dc=example,dc=com"; String searchPassword = "S3cr3tAdminPass"; String unpatched = ldapLogUnpatched(searchUser, searchPassword); String patched = ldapLogPatched(searchUser, searchPassword); // Unpatched log leaks first 3 chars of password String leaked = searchPassword.substring(0, 3); assert unpatched.contains(leaked) : "Unpatched log should contain first 3 chars: " + leaked; // Patched log must NOT contain any prefix of the password assert !patched.contains(leaked) : "Patched log must not leak any password chars"; assert patched.contains("***") : "Patched log must contain redaction marker"; // Both logs contain the admin DN (not a secret per se, but identity) assert patched.contains(searchUser) : "Log should still contain admin user DN"; System.out.println(" unpatched log: " + unpatched); System.out.println(" patched log: " + patched); System.out.println(" PASS"); } public static void main(String[] args) { testDolibarr0001(); testDolibarr0002(); testDolibarr0003(); testDolibarr0004(); testDolibarr0005(); System.out.println("\nAll 5 Dolibarr tests PASSED."); } }