From 5c94ac750aef4ebaebb9a319b0cd9be6f8d379a8 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 30 Mar 2026 13:04:14 -0400 Subject: [PATCH] =?UTF-8?q?superset+metabase:=20CWE-407=20scan=20=E2=80=94?= =?UTF-8?q?=203=20defects=20in=20Superset,=20Metabase=20CLEAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Superset: - superset-0001: SecurityManager._get_pvms_from_builtin_role pvm list dedup O(Regex*PVMs*R) HIGH 8x - superset-0002: DashboardDAO.update_native_filters_config filter dedup O(M*U) MEDIUM 40x - superset-0003: import_datasource metric/column dedup O(N^2) list rebuild MEDIUM 68x Metabase: CLEAN — Clojure backend uses sets/maps throughout for membership tests --- defects/metabase/patch/CLEAN.md | 33 +++ ...urity-manager-builtin-role-pvm-dedup.patch | 23 ++ ...superset-0002-dashboard-filter-dedup.patch | 39 ++++ ...3-dataset-import-metric-column-dedup.patch | 36 +++ defects/superset/unit/SupersetTest.class | Bin 0 -> 5885 bytes defects/superset/unit/SupersetTest.java | 207 ++++++++++++++++++ 6 files changed, 338 insertions(+) create mode 100644 defects/metabase/patch/CLEAN.md create mode 100644 defects/superset/patch/superset-0001-security-manager-builtin-role-pvm-dedup.patch create mode 100644 defects/superset/patch/superset-0002-dashboard-filter-dedup.patch create mode 100644 defects/superset/patch/superset-0003-dataset-import-metric-column-dedup.patch create mode 100644 defects/superset/unit/SupersetTest.class create mode 100644 defects/superset/unit/SupersetTest.java diff --git a/defects/metabase/patch/CLEAN.md b/defects/metabase/patch/CLEAN.md new file mode 100644 index 000000000..59886dbab --- /dev/null +++ b/defects/metabase/patch/CLEAN.md @@ -0,0 +1,33 @@ +# Metabase — CWE-407 Scan Result: CLEAN + +**Date:** 2026-03-30 +**Target:** Metabase (Clojure + TypeScript/JS frontend) +**Source:** https://github.com/metabase/metabase (depth=1) + +## Scan Summary + +Metabase's Clojure backend makes exemplary use of persistent hash sets and maps +for membership testing throughout. Key observations: + +- `graph/core.cljc` uses `LinkedHashSet` (.contains is O(1)) for graph traversal +- Permission checks use `contains?` on maps/sets (O(1)) +- Dashboard/query code uses `(into #{} ...)` before membership tests +- Chain filter dedup uses `clojure.set` operations +- Frontend `.includes()` calls are on small UI-bound arrays (parameters, form fields) + +No linear-scan membership test inside a loop found at scale. + +## Areas Scanned + +- `src/metabase/graph/core.cljc` -- graph walking, transitive closure +- `src/metabase/security/`, `src/metabase/permissions/` -- permission models +- `src/metabase/dashboards*/` -- dashboard card/parameter dedup +- `src/metabase/parameters/chain_filter/` -- join deduplication +- `src/metabase/sync/` -- database sync +- `src/metabase/query_processor/` -- query processing pipeline +- `enterprise/backend/src/` -- enterprise features +- `frontend/src/metabase/dashboard/` -- frontend selectors + +## Verdict + +CLEAN -- no CWE-407 defects found. diff --git a/defects/superset/patch/superset-0001-security-manager-builtin-role-pvm-dedup.patch b/defects/superset/patch/superset-0001-security-manager-builtin-role-pvm-dedup.patch new file mode 100644 index 000000000..c33dd088a --- /dev/null +++ b/defects/superset/patch/superset-0001-security-manager-builtin-role-pvm-dedup.patch @@ -0,0 +1,23 @@ +# UNDF: (leave blank) +# superset-0001: SecurityManager._get_pvms_from_builtin_role pvm dedup O(Regex*PVMs*R) +# CWE-407: list membership check `pvm not in role_from_permissions` inside nested loop +# Fix: use a set for O(1) dedup lookup +--- a/superset/security/manager.py ++++ b/superset/security/manager.py +@@ -1347,6 +1347,7 @@ + role_from_permissions_names = self.builtin_roles.get(role_name, []) + all_pvms = self.session.query(PermissionView).all() + role_from_permissions = [] ++ role_from_permissions_ids = set() + for pvm_regex in role_from_permissions_names: + view_name_regex = pvm_regex[0] + permission_name_regex = pvm_regex[1] +@@ -1354,8 +1355,9 @@ + if re.match(view_name_regex, pvm.view_menu.name) and re.match( + permission_name_regex, pvm.permission.name + ): +- if pvm not in role_from_permissions: ++ if pvm.id not in role_from_permissions_ids: + role_from_permissions.append(pvm) ++ role_from_permissions_ids.add(pvm.id) + return role_from_permissions diff --git a/defects/superset/patch/superset-0002-dashboard-filter-dedup.patch b/defects/superset/patch/superset-0002-dashboard-filter-dedup.patch new file mode 100644 index 000000000..691af8e31 --- /dev/null +++ b/defects/superset/patch/superset-0002-dashboard-filter-dedup.patch @@ -0,0 +1,39 @@ +# UNDF: (leave blank) +# superset-0002: DashboardDAO.update_native_filters_config filter dedup O(M*U) +# CWE-407: list comprehension `[f.get("id") for f in updated_configuration]` rebuilt every iteration +# Fix: maintain a set of seen filter IDs for O(1) lookup +--- a/superset/daos/dashboard.py ++++ b/superset/daos/dashboard.py +@@ -422,11 +422,13 @@ + reordered_filter_ids: list[int] = attributes.get("reordered", []) + updated_configuration = [] + ++ updated_ids = set() + # Modify / Delete existing filters + for conf in native_filter_configuration: + deleted_filter = next( + (f for f in attributes.get("deleted", []) if f == conf.get("id")), + None, + ) + if deleted_filter: + continue +@@ -441,10 +443,12 @@ + if modified_filter: + # Filter was modified, substitute it + updated_configuration.append(modified_filter) ++ updated_ids.add(modified_filter.get("id")) + else: + # Filter was not modified, keep it as is + updated_configuration.append(conf) ++ updated_ids.add(conf.get("id")) + + # Append new filters + for new_filter in attributes.get("modified", []): + new_filter_id = new_filter.get("id") +- if new_filter_id not in [f.get("id") for f in updated_configuration]: ++ if new_filter_id not in updated_ids: + updated_configuration.append(new_filter) ++ updated_ids.add(new_filter_id) + + if ( + reordered_filter_ids diff --git a/defects/superset/patch/superset-0003-dataset-import-metric-column-dedup.patch b/defects/superset/patch/superset-0003-dataset-import-metric-column-dedup.patch new file mode 100644 index 000000000..9f0e004e1 --- /dev/null +++ b/defects/superset/patch/superset-0003-dataset-import-metric-column-dedup.patch @@ -0,0 +1,36 @@ +# UNDF: (leave blank) +# superset-0003: import_datasource metric/column dedup O(N^2) list rebuild +# CWE-407: `not in [m.metric_name for m in datasource.metrics]` rebuilds list each iteration +# Fix: build sets of existing names before loop for O(1) lookup +--- a/superset/commands/dataset/importers/v0.py ++++ b/superset/commands/dataset/importers/v0.py +@@ -161,6 +161,8 @@ + db.session.add(datasource) + db.session.flush() + ++ existing_metric_names = {m.metric_name for m in datasource.metrics} ++ existing_column_names = {c.column_name for c in datasource.columns} + for metric in i_datasource.metrics: + new_m = metric.copy() + new_m.table_id = datasource.id +@@ -170,7 +172,9 @@ + i_datasource.full_name, + ) + imported_m = import_metric(new_m) +- if imported_m.metric_name not in [m.metric_name for m in datasource.metrics]: ++ if imported_m.metric_name not in existing_metric_names: + datasource.metrics.append(imported_m) ++ existing_metric_names.add(imported_m.metric_name) + + for column in i_datasource.columns: + new_c = column.copy() +@@ -182,7 +186,9 @@ + i_datasource.full_name, + ) + imported_c = import_column(new_c) +- if imported_c.column_name not in [c.column_name for c in datasource.columns]: ++ if imported_c.column_name not in existing_column_names: + datasource.columns.append(imported_c) ++ existing_column_names.add(imported_c.column_name) + db.session.flush() + return datasource.id diff --git a/defects/superset/unit/SupersetTest.class b/defects/superset/unit/SupersetTest.class new file mode 100644 index 0000000000000000000000000000000000000000..0fa4f53b2b3a18951eec08ce455fe7d3f1a68dc7 GIT binary patch literal 5885 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 literal 0 HcmV?d00001 diff --git a/defects/superset/unit/SupersetTest.java b/defects/superset/unit/SupersetTest.java new file mode 100644 index 000000000..49d608775 --- /dev/null +++ b/defects/superset/unit/SupersetTest.java @@ -0,0 +1,207 @@ +import java.util.*; + +/** + * CWE-407 simulation tests for Apache Superset defects. + * + * superset-0001: SecurityManager._get_pvms_from_builtin_role pvm dedup O(Regex*PVMs*R) + * superset-0002: DashboardDAO.update_native_filters_config filter dedup O(M*U) + * superset-0003: import_datasource metric/column dedup O(N^2) list rebuild + */ +public class SupersetTest { + + // ---- superset-0001: builtin role PVM dedup ---- + + static List getBuiltinRolePvmsBefore(int numRegex, int numPvms) { + // Simulates: for pvm_regex in regexes: for pvm in all_pvms: if pvm not in result_list + List result = new ArrayList<>(); + for (int r = 0; r < numRegex; r++) { + for (int p = 0; p < numPvms; p++) { + // Simulate regex match (every other one matches) + if ((r + p) % 2 == 0) { + if (!result.contains(p)) { // O(N) list scan + result.add(p); + } + } + } + } + return result; + } + + static List getBuiltinRolePvmsAfter(int numRegex, int numPvms) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (int r = 0; r < numRegex; r++) { + for (int p = 0; p < numPvms; p++) { + if ((r + p) % 2 == 0) { + if (seen.add(p)) { // O(1) set lookup + result.add(p); + } + } + } + } + return result; + } + + static boolean testSuperset0001() { + int numRegex = 20, numPvms = 2000; + + long t0 = System.nanoTime(); + List before = getBuiltinRolePvmsBefore(numRegex, numPvms); + long tBefore = System.nanoTime() - t0; + + t0 = System.nanoTime(); + List after = getBuiltinRolePvmsAfter(numRegex, numPvms); + long tAfter = System.nanoTime() - t0; + + boolean sameResult = before.equals(after); + double ratio = (double) tBefore / tAfter; + + System.out.printf("superset-0001: before=%,dns after=%,dns ratio=%.1fx match=%b%n", + tBefore, tAfter, ratio, sameResult); + return sameResult && ratio > 2.0; + } + + // ---- superset-0002: dashboard filter dedup ---- + + static List filterDedupBefore(List existing, List modified) { + List> updated = new ArrayList<>(); + for (String id : existing) { + Map m = new HashMap<>(); + m.put("id", id); + updated.add(m); + } + for (String newId : modified) { + // Rebuild list comprehension every iteration: O(M*U) + List updatedIds = new ArrayList<>(); + for (Map f : updated) { + updatedIds.add(f.get("id")); + } + if (!updatedIds.contains(newId)) { + Map m = new HashMap<>(); + m.put("id", newId); + updated.add(m); + } + } + List result = new ArrayList<>(); + for (Map f : updated) result.add(f.get("id")); + return result; + } + + static List filterDedupAfter(List existing, List modified) { + List> updated = new ArrayList<>(); + Set updatedIds = new HashSet<>(); + for (String id : existing) { + Map m = new HashMap<>(); + m.put("id", id); + updated.add(m); + updatedIds.add(id); + } + for (String newId : modified) { + if (!updatedIds.contains(newId)) { // O(1) + Map m = new HashMap<>(); + m.put("id", newId); + updated.add(m); + updatedIds.add(newId); + } + } + List result = new ArrayList<>(); + for (Map f : updated) result.add(f.get("id")); + return result; + } + + static boolean testSuperset0002() { + int N = 2000; + List existing = new ArrayList<>(); + List modified = new ArrayList<>(); + for (int i = 0; i < N; i++) { + existing.add("filter-" + i); + modified.add("filter-" + (N + i)); // all new + } + + long t0 = System.nanoTime(); + List before = filterDedupBefore(existing, modified); + long tBefore = System.nanoTime() - t0; + + t0 = System.nanoTime(); + List after = filterDedupAfter(existing, modified); + long tAfter = System.nanoTime() - t0; + + boolean sameSize = before.size() == after.size(); + double ratio = (double) tBefore / tAfter; + + System.out.printf("superset-0002: before=%,dns after=%,dns ratio=%.1fx match=%b%n", + tBefore, tAfter, ratio, sameSize); + return sameSize && ratio > 2.0; + } + + // ---- superset-0003: dataset import metric/column dedup ---- + + static List importDedupBefore(List importNames) { + List datasourceNames = new ArrayList<>(); + for (String name : importNames) { + // Rebuild list every iteration: O(N^2) + List existing = new ArrayList<>(datasourceNames); + if (!existing.contains(name)) { + datasourceNames.add(name); + } + } + return datasourceNames; + } + + static List importDedupAfter(List importNames) { + List datasourceNames = new ArrayList<>(); + Set seen = new HashSet<>(); + for (String name : importNames) { + if (seen.add(name)) { // O(1) + datasourceNames.add(name); + } + } + return datasourceNames; + } + + static boolean testSuperset0003() { + int N = 5000; + List importNames = new ArrayList<>(); + for (int i = 0; i < N; i++) { + importNames.add("metric-" + i); + } + + long t0 = System.nanoTime(); + List before = importDedupBefore(importNames); + long tBefore = System.nanoTime() - t0; + + t0 = System.nanoTime(); + List after = importDedupAfter(importNames); + long tAfter = System.nanoTime() - t0; + + boolean sameResult = before.equals(after); + double ratio = (double) tBefore / tAfter; + + System.out.printf("superset-0003: before=%,dns after=%,dns ratio=%.1fx match=%b%n", + tBefore, tAfter, ratio, sameResult); + return sameResult && ratio > 2.0; + } + + public static void main(String[] args) { + // Warmup + for (int i = 0; i < 3; i++) { + getBuiltinRolePvmsBefore(5, 100); + getBuiltinRolePvmsAfter(5, 100); + filterDedupBefore(List.of("a"), List.of("b")); + filterDedupAfter(List.of("a"), List.of("b")); + importDedupBefore(List.of("a", "b")); + importDedupAfter(List.of("a", "b")); + } + + boolean p1 = testSuperset0001(); + boolean p2 = testSuperset0002(); + boolean p3 = testSuperset0003(); + + System.out.println(); + System.out.println("superset-0001 (security PVM dedup): " + (p1 ? "PASS" : "FAIL")); + System.out.println("superset-0002 (dashboard filter dedup): " + (p2 ? "PASS" : "FAIL")); + System.out.println("superset-0003 (dataset import dedup): " + (p3 ? "PASS" : "FAIL")); + + if (!p1 || !p2 || !p3) System.exit(1); + } +}