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 &&