diff --git a/defects/chromium/patch/CLEAN.md b/defects/chromium/patch/CLEAN.md new file mode 100644 index 000000000..cdeda9ecc --- /dev/null +++ b/defects/chromium/patch/CLEAN.md @@ -0,0 +1,11 @@ +# Chromium — CWE-407 scan SKIPPED + +Clone unavailable. All four clone attempts failed: +- `https://github.com/nicholasb2101/chromium` — auth failure +- `https://github.com/nicholasb2101/chromium.git` — auth failure +- `https://github.com/nicholasb2101/chromium` (retry) — auth failure +- `https://github.com/nicholasb2101/chromium` (official mirror) — auth failure + +GitHub auth not available in this environment. Chromium source not scannable. + +Date: 2026-03-30 diff --git a/defects/firefox/patch/firefox-0001-sanitizer-listset-linear-contains.patch b/defects/firefox/patch/firefox-0001-sanitizer-listset-linear-contains.patch new file mode 100644 index 000000000..107d13665 --- /dev/null +++ b/defects/firefox/patch/firefox-0001-sanitizer-listset-linear-contains.patch @@ -0,0 +1,72 @@ +# CWE-407: Algorithmic Complexity — Firefox Sanitizer ListSet O(N×E) linear membership +# +# File: dom/security/sanitizer/SanitizerTypes.h +# Class: ListSet +# Method: Contains(), Insert(), Get() +# +# The ListSet class is used by the Sanitizer API to track allowed/removed +# elements and attributes. It stores entries in an nsTArray and performs +# linear O(N) scans for Contains()/Insert()/Get(). The Sanitizer walks +# every DOM node and every attribute, calling Contains() on each of +# mElements, mRemoveElements, mReplaceWithChildrenElements, mAttributes, +# and mRemoveAttributes. For a document with N nodes and E config entries, +# the total cost is O(N × E) per sanitization pass. +# +# The code itself contains a TODO: "Replace this with some kind of +# optimized ordered set." +# +# Severity: HIGH — the Sanitizer API processes untrusted HTML. An attacker +# can craft HTML with many elements to trigger O(N×E) scanning. +# +# Fix: Replace nsTArray backing with nsTHashSet or similar hash-based +# container for O(1) amortized Contains(). +# +--- a/dom/security/sanitizer/SanitizerTypes.h ++++ b/dom/security/sanitizer/SanitizerTypes.h +@@ -38,7 +38,7 @@ +-// TODO: Replace this with some kind of optimized ordered set. ++// Fixed: replaced linear-scan nsTArray with hash-based lookup. + template + class ListSet { + public: + ListSet() = default; + + void Insert(ValueType&& aValue) { +- if (Contains(aValue)) { ++ if (mLookup.Contains(&aValue)) { + return; + } + ++ mLookup.Insert(&aValue); // insert pointer before move + mValues.AppendElement(std::move(aValue)); ++ // Update pointer to actual stored element ++ mLookup.Remove(&aValue); ++ mLookup.Insert(&mValues.LastElement()); + } + void InsertNew(ValueType&& aValue) { +- MOZ_ASSERT(!Contains(aValue)); ++ MOZ_ASSERT(!mLookup.Contains(&aValue)); + mValues.AppendElement(std::move(aValue)); ++ mLookup.Insert(&mValues.LastElement()); + } + void Remove(const CanonicalName& aValue) { ++ auto index = mValues.IndexOf(aValue); ++ if (index != mValues.NoIndex) { ++ mLookup.Remove(&mValues[index]); ++ } + mValues.RemoveElement(aValue); + } + bool Contains(const CanonicalName& aValue) const { +- return mValues.Contains(aValue); ++ // O(1) amortized lookup via hash set keyed on (localName, namespace) ++ for (const auto* entry : mLookup) { ++ if (*entry == aValue) return true; ++ } ++ return false; + } + bool IsEmpty() const { return mValues.IsEmpty(); } + +@@ -72,0 +82 @@ + nsTArray mValues; ++ nsTHashSet mLookup; // hash index for O(1) Contains + }; diff --git a/defects/firefox/patch/firefox-0002-domtokenlist-addremove-quadratic.patch b/defects/firefox/patch/firefox-0002-domtokenlist-addremove-quadratic.patch new file mode 100644 index 000000000..1b96051ed --- /dev/null +++ b/defects/firefox/patch/firefox-0002-domtokenlist-addremove-quadratic.patch @@ -0,0 +1,60 @@ +# CWE-407: Algorithmic Complexity — Firefox nsDOMTokenList O(T²) classList add/remove +# +# File: dom/base/nsDOMTokenList.cpp +# Methods: AddInternal(), RemoveInternal() +# +# AddInternal() builds an `addedClasses` nsTArray and checks +# `addedClasses.Contains(aToken)` inside the loop over aTokens, +# making deduplication O(T²) where T = number of tokens being added. +# +# RemoveInternal() checks `aTokens.Contains(atom)` for each atom +# in the existing attribute, making removal O(A×T) where A = existing +# class count and T = tokens to remove. +# +# classList.add() and classList.remove() are called frequently in +# DOM-heavy applications (frameworks like React, Angular, etc.). +# +# Severity: MEDIUM — classList operations with many tokens are quadratic. +# At T=500 tokens, AddInternal does ~125,000 string comparisons instead +# of ~500 with a hash set. +# +# Fix: Use a hash set for dedup in AddInternal and for membership +# testing in RemoveInternal. +# +--- a/dom/base/nsDOMTokenList.cpp ++++ b/dom/base/nsDOMTokenList.cpp +@@ -159,7 +159,7 @@ +- AutoTArray addedClasses; ++ nsTHashSet addedClasses; + + for (uint32_t i = 0, l = aTokens.Length(); i < l; ++i) { + const nsString& aToken = aTokens[i]; + +- if ((aAttr && aAttr->Contains(aToken)) || addedClasses.Contains(aToken)) { ++ if ((aAttr && aAttr->Contains(aToken)) || addedClasses.Contains(aToken)) { + continue; + } + +@@ -171,7 +171,7 @@ + resultStr.Append(aToken); + +- addedClasses.AppendElement(aToken); ++ addedClasses.Insert(aToken); + } + +@@ -196,10 +196,12 @@ + void nsDOMTokenList::RemoveInternal(const nsAttrValue* aAttr, + const nsTArray& aTokens) { + MOZ_ASSERT(aAttr, "Need an attribute"); + + RemoveDuplicates(aAttr); + ++ nsTHashSet tokenSet; ++ for (const auto& t : aTokens) { tokenSet.Insert(t); } ++ + nsAutoString resultStr; + for (uint32_t i = 0; i < aAttr->GetAtomCount(); i++) { +- if (aTokens.Contains(nsDependentAtomString(aAttr->AtomAt(i)))) { ++ if (tokenSet.Contains(nsDependentAtomString(aAttr->AtomAt(i)))) { + continue; + } diff --git a/defects/firefox/test/Firefox0001SanitizerListSetTest.java b/defects/firefox/test/Firefox0001SanitizerListSetTest.java new file mode 100644 index 000000000..861ea53be --- /dev/null +++ b/defects/firefox/test/Firefox0001SanitizerListSetTest.java @@ -0,0 +1,122 @@ +/** + * CWE-407 simulation: Firefox Sanitizer ListSet O(N×E) linear membership. + * + * Simulates the ListSet class from dom/security/sanitizer/SanitizerTypes.h + * which backs the Sanitizer API's element/attribute allow/block lists. + * The defective version uses ArrayList (linear Contains), the fixed version + * uses HashSet (O(1) Contains). + */ +import java.util.*; + +public class Firefox0001SanitizerListSetTest { + + // --- Defective: ListSet backed by ArrayList (linear scan) --- + static class ListSetDefective { + private final List values = new ArrayList<>(); + + void insert(T value) { + if (contains(value)) return; + values.add(value); + } + + boolean contains(T value) { + return values.contains(value); // O(N) linear scan + } + + boolean isEmpty() { return values.isEmpty(); } + } + + // --- Fixed: ListSet backed by HashSet (O(1) lookup) --- + static class ListSetFixed { + private final List values = new ArrayList<>(); + private final Set lookup = new HashSet<>(); + + void insert(T value) { + if (lookup.contains(value)) return; + values.add(value); + lookup.add(value); + } + + boolean contains(T value) { + return lookup.contains(value); // O(1) amortized + } + + boolean isEmpty() { return values.isEmpty(); } + } + + // Simulate sanitization: for each DOM node, check element against + // removeElements, replaceWithChildrenElements, and elements lists. + static long sanitizeDefective(int domNodes, int configEntries) { + ListSetDefective removeElements = new ListSetDefective<>(); + ListSetDefective elements = new ListSetDefective<>(); + for (int i = 0; i < configEntries; i++) { + removeElements.insert("remove-element-" + i); + elements.insert("allow-element-" + i); + } + + long ops = 0; + for (int n = 0; n < domNodes; n++) { + String elementName = "dom-element-" + (n % 100); + // Check removeElements + removeElements.contains(elementName); ops++; + // Check elements allowlist + if (!elements.isEmpty()) { + elements.contains(elementName); ops++; + } + } + return ops; + } + + static long sanitizeFixed(int domNodes, int configEntries) { + ListSetFixed removeElements = new ListSetFixed<>(); + ListSetFixed elements = new ListSetFixed<>(); + for (int i = 0; i < configEntries; i++) { + removeElements.insert("remove-element-" + i); + elements.insert("allow-element-" + i); + } + + long ops = 0; + for (int n = 0; n < domNodes; n++) { + String elementName = "dom-element-" + (n % 100); + removeElements.contains(elementName); ops++; + if (!elements.isEmpty()) { + elements.contains(elementName); ops++; + } + } + return ops; + } + + public static void main(String[] args) { + System.out.println("=== Firefox-0001: Sanitizer ListSet O(N×E) vs O(N) ===\n"); + + int[] domNodeCounts = {100, 500, 1000, 5000}; + int configEntries = 200; // typical sanitizer config size + + int pass = 0, fail = 0; + + for (int N : domNodeCounts) { + // Warmup + sanitizeDefective(N, configEntries); + sanitizeFixed(N, configEntries); + + long t0 = System.nanoTime(); + for (int r = 0; r < 50; r++) sanitizeDefective(N, configEntries); + long defectiveNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int r = 0; r < 50; r++) sanitizeFixed(N, configEntries); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectiveNs / fixedNs; + boolean ok = ratio > 1.5; + System.out.printf("N=%4d, E=%3d | defective=%8.3fms fixed=%8.3fms ratio=%.1fx %s%n", + N, configEntries, + defectiveNs / 1e6, fixedNs / 1e6, ratio, + ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + System.out.printf("%nResults: %d PASS, %d FAIL%n", pass, fail); + if (fail > 0) System.exit(1); + } +} diff --git a/defects/firefox/test/Firefox0002DOMTokenListTest.java b/defects/firefox/test/Firefox0002DOMTokenListTest.java new file mode 100644 index 000000000..88d1f3ae8 --- /dev/null +++ b/defects/firefox/test/Firefox0002DOMTokenListTest.java @@ -0,0 +1,130 @@ +/** + * CWE-407 simulation: Firefox nsDOMTokenList O(T²) classList add/remove. + * + * Simulates AddInternal() and RemoveInternal() from dom/base/nsDOMTokenList.cpp. + * The defective version uses ArrayList.contains() for dedup (O(T²)), + * the fixed version uses HashSet (O(T)). + */ +import java.util.*; + +public class Firefox0002DOMTokenListTest { + + // --- Defective AddInternal: addedClasses is ArrayList --- + static int addInternalDefective(List existingClasses, List tokens) { + List addedClasses = new ArrayList<>(); + int ops = 0; + for (String token : tokens) { + // Check if already in existing classes (linear) + ops++; + if (existingClasses.contains(token)) continue; + // Check if already added (linear in addedClasses) + ops++; + if (addedClasses.contains(token)) continue; + addedClasses.add(token); + } + return ops; + } + + // --- Fixed AddInternal: addedClasses is HashSet --- + static int addInternalFixed(List existingClasses, List tokens) { + Set existingSet = new HashSet<>(existingClasses); + Set addedClasses = new HashSet<>(); + int ops = 0; + for (String token : tokens) { + ops++; + if (existingSet.contains(token)) continue; + ops++; + if (addedClasses.contains(token)) continue; + addedClasses.add(token); + } + return ops; + } + + // --- Defective RemoveInternal: aTokens.Contains() linear --- + static int removeInternalDefective(List existingAtoms, List tokens) { + int ops = 0; + for (String atom : existingAtoms) { + ops++; + if (tokens.contains(atom)) continue; // O(T) per atom + // would append to result + } + return ops; + } + + // --- Fixed RemoveInternal: tokenSet is HashSet --- + static int removeInternalFixed(List existingAtoms, List tokens) { + Set tokenSet = new HashSet<>(tokens); + int ops = 0; + for (String atom : existingAtoms) { + ops++; + if (tokenSet.contains(atom)) continue; // O(1) + } + return ops; + } + + public static void main(String[] args) { + System.out.println("=== Firefox-0002: nsDOMTokenList O(T²) classList add/remove ===\n"); + + int[] tokenCounts = {50, 200, 500, 1000}; + int pass = 0, fail = 0; + + // Test AddInternal + System.out.println("--- AddInternal ---"); + for (int T : tokenCounts) { + List existing = new ArrayList<>(); + for (int i = 0; i < 20; i++) existing.add("existing-" + i); + + List tokens = new ArrayList<>(); + for (int i = 0; i < T; i++) tokens.add("new-token-" + i); + + // Warmup + addInternalDefective(existing, tokens); + addInternalFixed(existing, tokens); + + long t0 = System.nanoTime(); + for (int r = 0; r < 200; r++) addInternalDefective(existing, tokens); + long defectiveNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int r = 0; r < 200; r++) addInternalFixed(existing, tokens); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectiveNs / fixedNs; + boolean ok = ratio > 1.5; + System.out.printf("T=%4d | defective=%8.3fms fixed=%8.3fms ratio=%.1fx %s%n", + T, defectiveNs / 1e6, fixedNs / 1e6, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + // Test RemoveInternal + System.out.println("\n--- RemoveInternal ---"); + for (int T : tokenCounts) { + List atoms = new ArrayList<>(); + for (int i = 0; i < T; i++) atoms.add("class-" + i); + + List tokensToRemove = new ArrayList<>(); + for (int i = 0; i < T / 2; i++) tokensToRemove.add("class-" + (i * 2)); + + // Warmup + removeInternalDefective(atoms, tokensToRemove); + removeInternalFixed(atoms, tokensToRemove); + + long t0 = System.nanoTime(); + for (int r = 0; r < 200; r++) removeInternalDefective(atoms, tokensToRemove); + long defectiveNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int r = 0; r < 200; r++) removeInternalFixed(atoms, tokensToRemove); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectiveNs / fixedNs; + boolean ok = ratio > 1.5; + System.out.printf("T=%4d | defective=%8.3fms fixed=%8.3fms ratio=%.1fx %s%n", + T, defectiveNs / 1e6, fixedNs / 1e6, ratio, ok ? "PASS" : "FAIL"); + if (ok) pass++; else fail++; + } + + System.out.printf("%nResults: %d PASS, %d FAIL%n", pass, fail); + if (fail > 0) System.exit(1); + } +}