You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2026/04/15 12:47:57 UTC

(superset) 01/01: fix(native-filters): prevent infinite recursion in filter scope tree traversal (#39355)

This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 305927df7b85cb25616bd5f8de8aa8f306dd3d3d
Author: Luiz Otavio <45...@users.noreply.github.com>
AuthorDate: Wed Apr 15 08:16:12 2026 -0300

    fix(native-filters): prevent infinite recursion in filter scope tree traversal (#39355)
    
    (cherry picked from commit 86575e129b72ab5bd9f23f59bb8ecad01eb55204)
---
 .../FiltersConfigForm/FilterScope/utils.test.ts    | 81 +++++++++++++++++++++-
 .../FiltersConfigForm/FilterScope/utils.ts         | 12 ++++
 2 files changed, 92 insertions(+), 1 deletion(-)

diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.test.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.test.ts
index 4ce48ed1cc7..94c1b3cdc85 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.test.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.test.ts
@@ -18,7 +18,7 @@
  */
 import { Layout, LayoutItem, Charts } from 'src/dashboard/types';
 import { VizType } from '@superset-ui/core';
-import { buildTree } from './utils';
+import { buildTree, getTreeCheckedItems } from './utils';
 import type { TreeItem } from './types';
 
 // The types defined for Layout and sub elements is not compatible with the data we get back fro a real dashboard layout
@@ -18048,6 +18048,85 @@ describe('Ensure buildTree does not throw runtime errors when encountering an in
     }).not.toThrow();
   });
 
+  test('Does not infinitely recurse when layout contains a cycle (nested tabs circular reference)', () => {
+    // Simulate a corrupted layout where TAB-A → TAB-B → TAB-A (cycle)
+    const circularLayout = {
+      ROOT_ID: {
+        id: 'ROOT_ID',
+        type: 'ROOT',
+        children: ['TAB-A'],
+        parents: [],
+      },
+      'TAB-A': {
+        id: 'TAB-A',
+        type: 'TAB',
+        children: ['TAB-B'],
+        parents: ['ROOT_ID'],
+        meta: { text: 'Tab A' },
+      },
+      'TAB-B': {
+        id: 'TAB-B',
+        type: 'TAB',
+        // Points back to TAB-A, creating a cycle
+        children: ['TAB-A'],
+        parents: ['ROOT_ID', 'TAB-A'],
+        meta: { text: 'Tab B' },
+      },
+    };
+
+    const rootNode = circularLayout.ROOT_ID;
+    const rootTreeItem: TreeItem = {
+      key: 'ROOT_ID',
+      title: 'Root',
+      children: [],
+      nodeType: 'TAB',
+    };
+
+    expect(() => {
+      buildTree(
+        rootNode as unknown as LayoutItem,
+        rootTreeItem,
+        circularLayout as unknown as Layout,
+        {} as Charts,
+        ['ROOT_ID', 'TAB-A', 'TAB-B'],
+        [],
+        () => 'title',
+      );
+    }).not.toThrow();
+  });
+
+  test('getTreeCheckedItems does not infinitely recurse when scope rootPath creates a cycle', () => {
+    // Simulate a corrupted layout where ROW-A → ROW-B → ROW-A (cycle via children)
+    const circularLayout = {
+      ROOT_ID: {
+        id: 'ROOT_ID',
+        type: 'ROOT',
+        children: ['ROW-A'],
+        parents: [],
+      },
+      'ROW-A': {
+        id: 'ROW-A',
+        type: 'ROW',
+        children: ['ROW-B'],
+        parents: ['ROOT_ID'],
+      },
+      'ROW-B': {
+        id: 'ROW-B',
+        type: 'ROW',
+        // Points back to ROW-A, creating a cycle
+        children: ['ROW-A'],
+        parents: ['ROOT_ID', 'ROW-A'],
+      },
+    };
+
+    expect(() => {
+      getTreeCheckedItems(
+        { rootPath: ['ROOT_ID'], excluded: [] },
+        circularLayout as unknown as Layout,
+      );
+    }).not.toThrow();
+  });
+
   test('Avoids runtime error with invalid inputs', () => {
     expect(() => {
       buildTree(
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts
index af62f19034b..8d61b956929 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils.ts
@@ -100,6 +100,7 @@ export const buildTree = (
   initiallyExcludedCharts: number[],
   buildTreeLeafTitle: BuildTreeLeafTitle,
   sliceEntities?: Record<number, Slice>,
+  visited: Set<string> = new Set(),
 ) => {
   if (!node) {
     return;
@@ -154,6 +155,10 @@ export const buildTree = (
 
   if (node.type !== CHART_TYPE) {
     node?.children?.forEach?.(child => {
+      if (visited.has(child)) {
+        return;
+      }
+      visited.add(child);
       const childNode = layout?.[child];
       if (childNode) {
         buildTree(
@@ -165,6 +170,7 @@ export const buildTree = (
           initiallyExcludedCharts,
           buildTreeLeafTitle,
           sliceEntities,
+          visited,
         );
       } else {
         logging.warn(
@@ -192,13 +198,19 @@ const checkTreeItem = (
   layout: Layout,
   items: string[],
   excluded: number[],
+  visited: Set<string> = new Set(),
 ) => {
   items.forEach(item => {
+    if (visited.has(item)) {
+      return;
+    }
+    visited.add(item);
     checkTreeItem(
       checkedItems,
       layout,
       addInvisibleParents(layout, item),
       excluded,
+      visited,
     );
     if (
       layout[item]?.type === CHART_TYPE &&