diff --git a/defects/erpnext-0001/patch/erpnext-0001-bom-get-children-list-dedup.patch b/defects/erpnext-0001/patch/erpnext-0001-bom-get-children-list-dedup.patch new file mode 100644 index 000000000..f172d05a0 --- /dev/null +++ b/defects/erpnext-0001/patch/erpnext-0001-bom-get-children-list-dedup.patch @@ -0,0 +1,24 @@ +# UNDF: UNDF-2026-000000862 +--- a/erpnext/manufacturing/doctype/bom/bom.py ++++ b/erpnext/manufacturing/doctype/bom/bom.py +@@ -920,11 +920,13 @@ + count = 0 + if not bom_list: + bom_list = [] + +- if self.name not in bom_list: ++ bom_set = set(bom_list) ++ ++ if self.name not in bom_set: + bom_list.append(self.name) ++ bom_set.add(self.name) + + while count < len(bom_list): + for child_bom in _get_children(bom_list[count]): +- if child_bom not in bom_list: ++ if child_bom not in bom_set: + bom_list.append(child_bom) ++ bom_set.add(child_bom) + count += 1 + bom_list.reverse() + return bom_list diff --git a/defects/erpnext-0001/test/test_erpnext_0001.java b/defects/erpnext-0001/test/test_erpnext_0001.java new file mode 100644 index 000000000..1fecdcf91 --- /dev/null +++ b/defects/erpnext-0001/test/test_erpnext_0001.java @@ -0,0 +1,132 @@ +import java.util.*; + +/** + * Unit test for erpnext-0001: BOM.get_children() list dedup O(B^2) + * + * Simulates the BOM explosion BFS traversal where child_bom membership + * is checked via list scan (defect) vs set lookup (fix). + * + * Defect: bom.py lines 924-931 — "if child_bom not in bom_list" where + * bom_list is a Python list, giving O(B) per check and O(B^2) overall. + * + * Fix: maintain a parallel set for O(1) membership checks. + */ +public class test_erpnext_0001 { + + // --- Defect: list-only BFS dedup --- + static List getBomChildrenDefect(Map> bomTree, String root) { + List bomList = new ArrayList<>(); + bomList.add(root); + int count = 0; + while (count < bomList.size()) { + List children = bomTree.getOrDefault(bomList.get(count), Collections.emptyList()); + for (String child : children) { + if (!bomList.contains(child)) { // O(N) per check + bomList.add(child); + } + } + count++; + } + Collections.reverse(bomList); + return bomList; + } + + // --- Fix: set-backed BFS dedup --- + static List getBomChildrenFixed(Map> bomTree, String root) { + List bomList = new ArrayList<>(); + Set bomSet = new HashSet<>(); + bomList.add(root); + bomSet.add(root); + int count = 0; + while (count < bomList.size()) { + List children = bomTree.getOrDefault(bomList.get(count), Collections.emptyList()); + for (String child : children) { + if (!bomSet.contains(child)) { // O(1) per check + bomList.add(child); + bomSet.add(child); + } + } + count++; + } + Collections.reverse(bomList); + return bomList; + } + + // Build a BOM tree with N levels, each having fanout children + static Map> buildBomTree(int levels, int fanout) { + Map> tree = new HashMap<>(); + int id = 0; + Queue queue = new LinkedList<>(); + String root = "BOM-" + (id++); + queue.add(root); + + for (int level = 0; level < levels && !queue.isEmpty(); level++) { + int size = queue.size(); + for (int i = 0; i < size; i++) { + String parent = queue.poll(); + List children = new ArrayList<>(); + for (int j = 0; j < fanout; j++) { + String child = "BOM-" + (id++); + children.add(child); + queue.add(child); + } + tree.put(parent, children); + } + } + return tree; + } + + public static void main(String[] args) { + System.out.println("=== erpnext-0001: BOM get_children list dedup O(B^2) ===\n"); + + // Correctness test + Map> smallTree = new HashMap<>(); + smallTree.put("BOM-ROOT", Arrays.asList("BOM-A", "BOM-B")); + smallTree.put("BOM-A", Arrays.asList("BOM-C", "BOM-B")); // BOM-B appears twice + smallTree.put("BOM-B", Arrays.asList("BOM-D")); + smallTree.put("BOM-C", Collections.emptyList()); + smallTree.put("BOM-D", Collections.emptyList()); + + List defectResult = getBomChildrenDefect(smallTree, "BOM-ROOT"); + List fixedResult = getBomChildrenFixed(smallTree, "BOM-ROOT"); + + assert defectResult.equals(fixedResult) : + "Results differ: defect=" + defectResult + " fixed=" + fixedResult; + System.out.println("Correctness: PASS (both produce " + fixedResult + ")"); + + // Performance test: BOM tree with 500 unique BOMs + int levels = 5, fanout = 3; + Map> bigTree = buildBomTree(levels, fanout); + String root = "BOM-0"; + int totalBoms = bigTree.values().stream().mapToInt(List::size).sum() + 1; + System.out.println("BOM tree: " + totalBoms + " nodes, " + levels + " levels, fanout=" + fanout); + + // Warmup + for (int i = 0; i < 5; i++) { + getBomChildrenDefect(bigTree, root); + getBomChildrenFixed(bigTree, root); + } + + int iterations = 500; + long t0 = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + getBomChildrenDefect(bigTree, root); + } + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + getBomChildrenFixed(bigTree, root); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations); + System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations); + System.out.printf("Ratio: %.1fx%n", ratio); + + boolean pass = ratio > 1.5; + System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)"); + assert pass : "Expected speedup > 1.5x, got " + ratio; + } +} diff --git a/defects/erpnext-0002/patch/erpnext-0002-serial-batch-list-dedup.patch b/defects/erpnext-0002/patch/erpnext-0002-serial-batch-list-dedup.patch new file mode 100644 index 000000000..2bedfbdbd --- /dev/null +++ b/defects/erpnext-0002/patch/erpnext-0002-serial-batch-list-dedup.patch @@ -0,0 +1,27 @@ +# UNDF: UNDF-2026-000000863 +--- a/erpnext/stock/serial_batch_bundle.py ++++ b/erpnext/stock/serial_batch_bundle.py +@@ -1556,13 +1556,17 @@ + def get_serial_batch_list_from_item(item): + serial_list, batch_list = [], [] ++ serial_set, batch_set = set(), set() + if item.serial_and_batch_bundle: + table = frappe.qb.DocType("Serial and Batch Entry") + query = ( + frappe.qb.from_(table) + .select(table.serial_no, table.batch_no) + .where(table.parent == item.serial_and_batch_bundle) + ) + result = query.run(as_dict=True) + + for row in result: +- if row.serial_no and row.serial_no not in serial_list: ++ if row.serial_no and row.serial_no not in serial_set: + serial_list.append(row.serial_no) +- if row.batch_no and row.batch_no not in batch_list: ++ serial_set.add(row.serial_no) ++ if row.batch_no and row.batch_no not in batch_set: + batch_list.append(row.batch_no) ++ batch_set.add(row.batch_no) + else: + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/defects/erpnext-0002/test/test_erpnext_0002.java b/defects/erpnext-0002/test/test_erpnext_0002.java new file mode 100644 index 000000000..57ad8b1a7 --- /dev/null +++ b/defects/erpnext-0002/test/test_erpnext_0002.java @@ -0,0 +1,95 @@ +import java.util.*; + +/** + * Unit test for erpnext-0002: serial_batch_bundle.get_serial_batch_list_from_item O(N^2) + * + * Simulates serial number dedup where each serial_no is checked via + * list scan (defect) vs set lookup (fix). + * + * Defect: serial_batch_bundle.py lines 1567-1571 — "if row.serial_no not in serial_list" + * where serial_list is a Python list, giving O(N) per check and O(N^2) overall. + * + * Fix: maintain a parallel set for O(1) membership checks. + */ +public class test_erpnext_0002 { + + // --- Defect: list-only dedup --- + static List dedupSerialDefect(List serials) { + List serialList = new ArrayList<>(); + for (String sn : serials) { + if (sn != null && !serialList.contains(sn)) { // O(N) per check + serialList.add(sn); + } + } + return serialList; + } + + // --- Fix: set-backed dedup --- + static List dedupSerialFixed(List serials) { + List serialList = new ArrayList<>(); + Set serialSet = new HashSet<>(); + for (String sn : serials) { + if (sn != null && !serialSet.contains(sn)) { // O(1) per check + serialList.add(sn); + serialSet.add(sn); + } + } + return serialList; + } + + static List generateSerials(int n) { + List serials = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + serials.add("SN-" + String.format("%06d", i % (n / 2 + 1))); // ~50% duplicates + } + return serials; + } + + public static void main(String[] args) { + System.out.println("=== erpnext-0002: serial_batch_bundle dedup O(N^2) ===\n"); + + // Correctness test + List input = Arrays.asList("SN-001", "SN-002", "SN-001", "SN-003", "SN-002"); + List expected = Arrays.asList("SN-001", "SN-002", "SN-003"); + + List defectResult = dedupSerialDefect(input); + List fixedResult = dedupSerialFixed(input); + + assert defectResult.equals(expected) : "Defect result wrong: " + defectResult; + assert fixedResult.equals(expected) : "Fixed result wrong: " + fixedResult; + System.out.println("Correctness: PASS"); + + // Performance test: 5000 serial numbers + int N = 5000; + List serials = generateSerials(N); + System.out.println("Serial count: " + N + " (~50% duplicates)"); + + // Warmup + for (int i = 0; i < 5; i++) { + dedupSerialDefect(serials); + dedupSerialFixed(serials); + } + + int iterations = 50; + long t0 = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + dedupSerialDefect(serials); + } + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + dedupSerialFixed(serials); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations); + System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations); + System.out.printf("Ratio: %.1fx%n", ratio); + + boolean pass = ratio > 3.0; + System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)"); + assert pass : "Expected speedup > 3x, got " + ratio; + } +} diff --git a/defects/odoo/CLEAN.md b/defects/odoo/CLEAN.md new file mode 100644 index 000000000..69bb635bf --- /dev/null +++ b/defects/odoo/CLEAN.md @@ -0,0 +1,25 @@ +# Odoo — CWE-407 Scan Result: CLEAN + +**Scanned:** 2026-03-30 +**Source:** https://github.com/odoo/odoo (v19.0) +**Scanner:** manual CWE-407 audit + +## Summary + +No CWE-407 (algorithmic complexity via linear membership test in a loop) defects found. + +## Notes + +Odoo's codebase is well-protected against this class of defect: + +- ORM core (`odoo/orm/`) uses `set`, `frozenset`, `OrderedSet`, and `dict` consistently + for membership tests in loops +- Recordsets implement set-based `__contains__` (O(1)) +- `tools/misc.py` `unique()` helper uses `set` for dedup +- Module loading uses `dict`/`set` for loaded module tracking +- Record rules use SQL-based evaluation, not list scanning +- Field dependency tracking uses `Collector` (dict of tuples) with small per-key sizes +- Access control uses SQL queries, not list iteration + +The `Collector.add()` method in `tools/misc.py` does `if val not in vals` on a tuple, +but it is only called from field setup (not a hot loop) and tuple sizes are typically 1-3. diff --git a/defects/ofbiz-0001/patch/ofbiz-0001-payment-retry-list-dedup.patch b/defects/ofbiz-0001/patch/ofbiz-0001-payment-retry-list-dedup.patch new file mode 100644 index 000000000..9494ca345 --- /dev/null +++ b/defects/ofbiz-0001/patch/ofbiz-0001-payment-retry-list-dedup.patch @@ -0,0 +1,19 @@ +# UNDF: UNDF-2026-000000864 +--- a/applications/accounting/src/main/java/org/apache/ofbiz/accounting/payment/PaymentGatewayServices.java ++++ b/applications/accounting/src/main/java/org/apache/ofbiz/accounting/payment/PaymentGatewayServices.java +@@ -2711,7 +2711,7 @@ + + try (EntityListIterator eli = eq.queryIterator()) { +- List processList = new LinkedList<>(); ++ Set processList = new HashSet<>(); + if (eli != null) { + Debug.logInfo("Processing failed order re-auth(s)", MODULE); + GenericValue value = null; +@@ -2752,7 +2752,7 @@ + + try (EntityListIterator eli = eq.queryIterator()) { +- List processList = new LinkedList<>(); ++ Set processList = new HashSet<>(); + if (eli != null) { + Debug.logInfo("Processing failed order re-auth(s)", MODULE); + GenericValue value = null; diff --git a/defects/ofbiz-0001/test/test_ofbiz_0001.java b/defects/ofbiz-0001/test/test_ofbiz_0001.java new file mode 100644 index 000000000..65a6b36a7 --- /dev/null +++ b/defects/ofbiz-0001/test/test_ofbiz_0001.java @@ -0,0 +1,94 @@ +import java.util.*; + +/** + * Unit test for ofbiz-0001: PaymentGatewayServices processList LinkedList.contains O(N^2) + * + * Simulates the order dedup pattern in retryFailedAuths / retryFailedAuthNsfs + * where orderId membership is checked via LinkedList.contains (defect) vs + * HashSet.contains (fix). + * + * Defect: PaymentGatewayServices.java lines 2714-2724, 2755-2765 — + * "if (!processList.contains(orderId))" where processList is a LinkedList, + * giving O(N) per check inside a while loop over DB results → O(N^2). + * + * Fix: use HashSet for O(1) contains. + */ +public class test_ofbiz_0001 { + + // --- Defect: LinkedList dedup --- + static int processOrdersDefect(List orderIds) { + List processList = new LinkedList<>(); + int processed = 0; + for (String orderId : orderIds) { + if (!processList.contains(orderId)) { // O(N) per check + processList.add(orderId); + processed++; + } + } + return processed; + } + + // --- Fix: HashSet dedup --- + static int processOrdersFixed(List orderIds) { + Set processList = new HashSet<>(); + int processed = 0; + for (String orderId : orderIds) { + if (!processList.contains(orderId)) { // O(1) per check + processList.add(orderId); + processed++; + } + } + return processed; + } + + static List generateOrderIds(int n, int uniqueOrders) { + List ids = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + ids.add("ORD-" + String.format("%06d", i % uniqueOrders)); + } + return ids; + } + + public static void main(String[] args) { + System.out.println("=== ofbiz-0001: PaymentGatewayServices processList O(N^2) ===\n"); + + // Correctness test + List input = Arrays.asList("ORD-001", "ORD-002", "ORD-001", "ORD-003"); + assert processOrdersDefect(input) == 3 : "Defect: wrong unique count"; + assert processOrdersFixed(input) == 3 : "Fixed: wrong unique count"; + System.out.println("Correctness: PASS"); + + // Performance test: 10000 payment preferences, 2000 unique orders + int N = 10000, uniqueOrders = 2000; + List orderIds = generateOrderIds(N, uniqueOrders); + System.out.println("Payment preferences: " + N + ", unique orders: " + uniqueOrders); + + // Warmup + for (int i = 0; i < 5; i++) { + processOrdersDefect(orderIds); + processOrdersFixed(orderIds); + } + + int iterations = 50; + long t0 = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + processOrdersDefect(orderIds); + } + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + processOrdersFixed(orderIds); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations); + System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations); + System.out.printf("Ratio: %.1fx%n", ratio); + + boolean pass = ratio > 5.0; + System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)"); + assert pass : "Expected speedup > 5x, got " + ratio; + } +} diff --git a/defects/ofbiz-0002/patch/ofbiz-0002-order-return-list-dedup.patch b/defects/ofbiz-0002/patch/ofbiz-0002-order-return-list-dedup.patch new file mode 100644 index 000000000..72cef105f --- /dev/null +++ b/defects/ofbiz-0002/patch/ofbiz-0002-order-return-list-dedup.patch @@ -0,0 +1,32 @@ +# UNDF: UNDF-2026-000000865 +--- a/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderReturnServices.java ++++ b/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderReturnServices.java +@@ -2352,7 +2352,7 @@ +- List paymentList = new LinkedList<>(); ++ Set paymentSet = new HashSet<>(); + for (GenericValue returnItem : returnItems) { + String orderId = returnItem.getString("orderId"); + try { +@@ -2361,8 +2361,8 @@ + GenericValue payment = returnItemResponse.getRelatedOne("Payment", false); + if ((payment != null) && (payment.getBigDecimal("amount") != null) +- && !paymentList.contains(payment.get("paymentId"))) { ++ && !paymentSet.contains(payment.getString("paymentId"))) { + UtilMisc.addToBigDecimalInMap(returnAmountByOrder, orderId, payment.getBigDecimal("amount")); +- paymentList.add(payment.getString("paymentId")); // make sure we don't add duplicated payment amount ++ paymentSet.add(payment.getString("paymentId")); // make sure we don't add duplicated payment amount + +--- a/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderReadHelper.java ++++ b/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderReadHelper.java +@@ -2235,11 +2235,12 @@ +- List returnHeaderList = new LinkedList<>(); ++ Set returnHeaderSet = new LinkedHashSet<>(); + ... +- if (orderId.equals(returnedItem.getString("orderId")) && (!returnHeaderList.contains(returnedItem.getString("returnId")))) { +- returnHeaderList.add(returnedItem.getString("returnId")); ++ if (orderId.equals(returnedItem.getString("orderId")) && (!returnHeaderSet.contains(returnedItem.getString("returnId")))) { ++ returnHeaderSet.add(returnedItem.getString("returnId")); + } + } +- for (String returnId : returnHeaderList) { ++ for (String returnId : returnHeaderSet) { diff --git a/defects/ofbiz-0002/test/test_ofbiz_0002.java b/defects/ofbiz-0002/test/test_ofbiz_0002.java new file mode 100644 index 000000000..12a4920c4 --- /dev/null +++ b/defects/ofbiz-0002/test/test_ofbiz_0002.java @@ -0,0 +1,90 @@ +import java.util.*; + +/** + * Unit test for ofbiz-0002: OrderReturnServices/OrderReadHelper LinkedList.contains O(N^2) + * + * Simulates payment ID and return header ID dedup where membership is checked + * via LinkedList.contains (defect) vs HashSet.contains (fix). + * + * Defect: OrderReturnServices.java line 2364 — paymentList (LinkedList) .contains() + * OrderReadHelper.java line 2247 — returnHeaderList (LinkedList) .contains() + * + * Fix: use HashSet/LinkedHashSet for O(1) contains. + */ +public class test_ofbiz_0002 { + + // --- Defect: LinkedList dedup --- + static List dedupPaymentsDefect(List paymentIds) { + List paymentList = new LinkedList<>(); + for (String pid : paymentIds) { + if (!paymentList.contains(pid)) { // O(N) + paymentList.add(pid); + } + } + return paymentList; + } + + // --- Fix: HashSet dedup --- + static Set dedupPaymentsFixed(List paymentIds) { + Set paymentSet = new LinkedHashSet<>(); + for (String pid : paymentIds) { + paymentSet.add(pid); // O(1) contains + add + } + return paymentSet; + } + + static List generatePaymentIds(int n, int uniquePayments) { + List ids = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + ids.add("PMT-" + String.format("%06d", i % uniquePayments)); + } + return ids; + } + + public static void main(String[] args) { + System.out.println("=== ofbiz-0002: OrderReturnServices/OrderReadHelper dedup O(N^2) ===\n"); + + // Correctness test + List input = Arrays.asList("PMT-001", "PMT-002", "PMT-001", "PMT-003"); + List defectResult = dedupPaymentsDefect(input); + Set fixedResult = dedupPaymentsFixed(input); + + assert defectResult.size() == 3 : "Defect: wrong count"; + assert fixedResult.size() == 3 : "Fixed: wrong count"; + assert new LinkedHashSet<>(defectResult).equals(fixedResult) : "Results differ"; + System.out.println("Correctness: PASS"); + + // Performance test: 5000 return items, 1000 unique payments + int N = 5000, uniquePayments = 1000; + List paymentIds = generatePaymentIds(N, uniquePayments); + System.out.println("Return items: " + N + ", unique payments: " + uniquePayments); + + // Warmup + for (int i = 0; i < 5; i++) { + dedupPaymentsDefect(paymentIds); + dedupPaymentsFixed(paymentIds); + } + + int iterations = 100; + long t0 = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + dedupPaymentsDefect(paymentIds); + } + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + dedupPaymentsFixed(paymentIds); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("Defect: %,d ns / %d iters%n", defectNs, iterations); + System.out.printf("Fixed: %,d ns / %d iters%n", fixedNs, iterations); + System.out.printf("Ratio: %.1fx%n", ratio); + + boolean pass = ratio > 5.0; + System.out.println("\nResult: " + (pass ? "PASS" : "FAIL") + " (ratio=" + String.format("%.1f", ratio) + "x)"); + assert pass : "Expected speedup > 5x, got " + ratio; + } +}