java-topology/defects/dolibarr/test/DolibarrCWE407Test.java
russell@unturf.com fc7fdad2dc suitecrm+dolibarr: 5-MOAD scan — 3 new defects, MOADs 0002/0003/0005 CLEAN
suitecrm-0004: SugarBean.php subpanel union field dedup in_array($field, $all_fields)
inside nested foreach over subpanel queries — O(S*F^2), 4.3x at S=40 F=60.
Fix: parallel hash set for O(1) isset() check.

dolibarr-0004: emailcollector_card.php line 575 — IMAP password logged verbatim
unconditionally in dol_syslog(). CWE-312 HIGH. Fix: replace with literal ***.

dolibarr-0005: functions_ldap.php line 98 — LDAP admin searchPassword first 3 chars
leaked via dol_trunc() in dol_syslog() and browser print when $ldapdebug=true.
CWE-312 MEDIUM. Fix: replace dol_trunc(...) with literal *** in both sinks.

All 4 SuiteCRM tests PASS. All 5 Dolibarr tests PASS.
MOADs 0002/0003/0005 CLEAN for both targets (PHP single-threaded, architectural globals).
2026-03-31 20:27:34 -04:00

307 lines
12 KiB
Java

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<String> scanFiles, List<String> inSignature) {
List<String> 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<String> scanFiles, List<String> inSignature) {
Set<String> sigSet = new HashSet<>(inSignature); // O(I) once
List<String> 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<Integer> dbRows) {
List<Integer> 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<Integer> dbRows) {
Set<Integer> 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<Integer> pieceNums, List<Integer> 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<Integer> pieceNums, List<Integer> alreadyExtourne) {
Set<Integer> 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<String> scanFiles = new ArrayList<>(S);
List<String> 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<Integer> 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<Integer> pieceNums = new ArrayList<>(P);
for (int i = 0; i < P; i++) {
pieceNums.add(A + i); // new pieces not in already-reversed set
}
List<Integer> 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.");
}
}