java-topology/defects/invoiceninja-0001/test/InvoiceNinjaTest.java
russell@unturf.com 0f13a71cb4 invoiceninja: 3 defects (MOAD-0001/0003/0004); onlyoffice: 1 defect (MOAD-0001); all 5 MOADs scanned
invoiceninja-0001: S3Cleanup in_array O(D*C) across 3 DB shards, fix: array_flip+isset O(D+C), 9499x at D=C=10000
invoiceninja-0002: CheckoutCom webhook logs $request->all() on HMAC mismatch (CWE-312), exposes card fingerprints/BIN
invoiceninja-0003: 20 Mailable build() methods missing App::forgetInstance('translator') before setLocale, Octane locale leak (MOAD-0003)
onlyoffice-0001: Table.js 8x Array.indexOf inside for(curRow) loops O(R*A), fix: new Set+has O(R+A), 375x at R=1000

4/4 unit tests PASS
2026-03-31 20:53:13 -04:00

139 lines
5.5 KiB
Java

package unit;
import java.util.*;
/**
* InvoiceNinjaTest -- CWE-407 benchmark for invoiceninja-0001.
*
* invoiceninja-0001: S3Cleanup.handle() O(D*C)
* handle() collects company_key values from three DB shards into a plain
* PHP array ($merged), then iterates every S3 directory calling
* in_array($dir, $merged). PHP in_array on a plain array is O(C) linear
* scan, so the loop is O(D * C) where D = S3 directories and C = total
* company keys across all shards.
*
* For a hosted InvoiceNinja instance at scale: thousands of companies across
* 3 shards, thousands of S3 directories. At D=10000, C=10000 this is
* 100,000,000 string comparisons per cleanup run.
*
* Fix: convert $merged to a hash set via array_flip() (O(C) once), then
* replace in_array($dir, $merged) with isset($merged[$dir]) -- O(1) per
* lookup, O(D + C) total.
*
* File: app/Console/Commands/S3Cleanup.php
* Method: handle()
* Lines: 62, 69
*/
public class InvoiceNinjaTest {
// -----------------------------------------------------------------------
// invoiceninja-0001: S3Cleanup in_array O(D*C) -> isset O(D+C)
// -----------------------------------------------------------------------
/** Unpatched: in_array linear scan of merged company keys per directory. */
static long s3CleanupUnpatched(int numDirectories, int numCompanyKeys) {
// Build merged company key list (plain array)
List<String> mergedKeys = new ArrayList<>(numCompanyKeys);
for (int i = 0; i < numCompanyKeys; i++) {
mergedKeys.add("companykey" + i);
}
// Build directories list - some match, most don't
List<String> directories = new ArrayList<>(numDirectories);
for (int i = 0; i < numDirectories; i++) {
// Every 10th dir matches a company key; the rest are orphans
if (i % 10 == 0 && i < numCompanyKeys) {
directories.add("companykey" + i);
} else {
directories.add("orphan-dir-" + i);
}
}
long ops = 0;
// Simulate foreach($directories) { if (!in_array($dir, $merged)) { delete } }
for (String dir : directories) {
// in_array scans merged linearly
for (String key : mergedKeys) {
ops++;
if (dir.equals(key)) {
break;
}
}
}
return ops;
}
/** Patched: array_flip + isset, O(1) lookup per directory. */
static long s3CleanupPatched(int numDirectories, int numCompanyKeys) {
// Build merged company key list
List<String> mergedKeysList = new ArrayList<>(numCompanyKeys);
for (int i = 0; i < numCompanyKeys; i++) {
mergedKeysList.add("companykey" + i);
}
// Simulate array_flip -> HashSet/HashMap for O(1) lookup
Set<String> mergedSet = new HashSet<>(mergedKeysList);
// Build directories list
List<String> directories = new ArrayList<>(numDirectories);
for (int i = 0; i < numDirectories; i++) {
if (i % 10 == 0 && i < numCompanyKeys) {
directories.add("companykey" + i);
} else {
directories.add("orphan-dir-" + i);
}
}
long ops = 0;
// Simulate foreach($directories) { if (!isset($merged[$dir])) { delete } }
for (String dir : directories) {
ops++; // O(1) hash lookup
mergedSet.contains(dir); // actual O(1)
}
return ops;
}
static void test(String label, int dirs, int keys) {
long unpatched = s3CleanupUnpatched(dirs, keys);
long patched = s3CleanupPatched(dirs, keys);
double ratio = (double) unpatched / patched;
System.out.printf(" [%s] D=%d C=%d unpatched=%d patched=%d ratio=%.1fx%n",
label, dirs, keys, unpatched, patched, ratio);
if (ratio < 2.0) {
throw new AssertionError(label + ": expected ratio >= 2x, got " + ratio);
}
}
public static void main(String[] args) {
System.out.println("invoiceninja-0001: S3Cleanup in_array O(D*C) -> isset O(D+C)");
System.out.println();
// Correctness check: both paths agree on which dirs are orphans
List<String> keys100 = new ArrayList<>();
for (int i = 0; i < 100; i++) keys100.add("companykey" + i);
Set<String> keysSet = new HashSet<>(keys100);
// Directories 0,10,20,...,90 match keys (10 matches); 190 are orphans
List<String> orphans = new ArrayList<>();
for (int i = 0; i < 200; i++) {
String dir = (i % 10 == 0 && i < 100) ? "companykey" + i : "orphan-dir-" + i;
if (!keysSet.contains(dir)) orphans.add(dir);
}
int expectedOrphans = 190; // 200 dirs - 10 matching keys
System.out.println(" Correctness: orphan count = " + orphans.size() + " (expected " + expectedOrphans + ")");
if (orphans.size() != expectedOrphans) {
throw new AssertionError("Correctness FAIL: expected " + expectedOrphans + " orphans, got " + orphans.size());
}
System.out.println(" Correctness: PASS");
System.out.println();
System.out.println("Performance benchmarks:");
test("small", 1_000, 1_000);
test("medium", 5_000, 5_000);
test("large", 10_000, 10_000);
test("xlarge", 50_000, 50_000);
System.out.println();
System.out.println("PASS");
}
}