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
139 lines
5.5 KiB
Java
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");
|
|
}
|
|
}
|