You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by pm...@apache.org on 2026/06/03 11:56:11 UTC

(superset) branch master updated: fix(views): enforce per-chart access check in legacy form_data endpoint (#40497)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new b8a2f925ee4 fix(views): enforce per-chart access check in legacy form_data endpoint (#40497)
b8a2f925ee4 is described below

commit b8a2f925ee4b9401084b4ca238be9909f7a62f4f
Author: Shaitan <10...@users.noreply.github.com>
AuthorDate: Wed Jun 3 12:56:03 2026 +0100

    fix(views): enforce per-chart access check in legacy form_data endpoint (#40497)
    
    Co-authored-by: Claude Opus 4.7 <no...@anthropic.com>
---
 superset/views/api.py                       | 19 ++++++++----
 tests/integration_tests/charts/api_tests.py | 46 +++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+), 6 deletions(-)

diff --git a/superset/views/api.py b/superset/views/api.py
index c24a102d65e..e1c36bf71f3 100644
--- a/superset/views/api.py
+++ b/superset/views/api.py
@@ -24,13 +24,14 @@ from flask_appbuilder.api import rison as parse_rison
 from flask_appbuilder.security.decorators import has_access_api
 from flask_babel import lazy_gettext as _
 
-from superset import db, event_logger
+from superset import event_logger
 from superset.commands.chart.exceptions import (
+    ChartNotFoundError,
     TimeRangeAmbiguousError,
     TimeRangeParseFailError,
 )
+from superset.daos.chart import ChartDAO
 from superset.legacy import update_time_range
-from superset.models.slice import Slice
 from superset.superset_typing import FlaskResponse
 from superset.utils import json
 from superset.utils.date_parser import get_since_until
@@ -85,11 +86,17 @@ class Api(BaseSupersetView):
         Get the form_data stored in the database for existing slice.
         params: slice_id: integer
         """
-        form_data = {}
+        form_data: dict[str, Any] = {}
         if slice_id := request.args.get("slice_id"):
-            slc = db.session.query(Slice).filter_by(id=slice_id).one_or_none()
-            if slc:
-                form_data = slc.form_data.copy()
+            # Reuse ChartDAO.get_by_id_or_uuid so this endpoint applies the
+            # same ChartFilter (dataset-scoped) as ChartRestApi.get. Both a
+            # missing chart and a chart the caller cannot access surface as
+            # ChartNotFoundError, mapped to 404 so the status code cannot be
+            # used to distinguish the two cases.
+            try:
+                form_data = ChartDAO.get_by_id_or_uuid(slice_id).form_data.copy()
+            except ChartNotFoundError:
+                return self.json_response({}, 404)
 
         update_time_range(form_data)
 
diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py
index 699a126f6b8..47fc2712625 100644
--- a/tests/integration_tests/charts/api_tests.py
+++ b/tests/integration_tests/charts/api_tests.py
@@ -1769,6 +1769,52 @@ class TestChartApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCase):
         if slice:
             assert data["slice_id"] == slice.id
 
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_query_form_data_no_data_access(self):
+        """
+        Chart API: query_form_data must refuse callers without
+        datasource_access on the chart's underlying dataset. Mirrors
+        the existing test_get_chart_no_data_access guard on
+        ChartRestApi (which also returns 404 to avoid leaking chart
+        existence to unauthorised callers).
+        """
+        self.login(GAMMA_USERNAME)
+        chart_no_access = (
+            db.session.query(Slice)
+            .filter_by(slice_name="Girl Name Cloud")
+            .one_or_none()
+        )
+        assert chart_no_access is not None, (
+            "fixture load_birth_names_dashboard_with_slices did not "
+            "create the 'Girl Name Cloud' slice"
+        )
+        uri = f"api/v1/form_data/?slice_id={chart_no_access.id}"
+        rv = self.client.get(uri)
+        # Match ChartRestApi.get: 404 for both missing AND forbidden so
+        # the endpoint cannot be used to enumerate chart IDs.
+        assert rv.status_code == 404, (
+            f"Gamma user without datasource_access should get 404 "
+            f"(status={rv.status_code}, body={rv.data[:200]!r})"
+        )
+        # Defence in depth: even if a future regression returns a non-
+        # 200 status with a partially-filled error envelope, ensure the
+        # caller cannot recover form_data fields.
+        assert b"datasource" not in rv.data
+        assert b"adhoc_filters" not in rv.data
+        assert b"viz_type" not in rv.data
+
+    def test_query_form_data_missing_slice(self):
+        """
+        Chart API: a non-existent slice_id must return the same 404 as a
+        forbidden one, so the status code cannot be used to enumerate
+        which slice IDs exist.
+        """
+        self.login(ADMIN_USERNAME)
+        max_id = db.session.query(func.max(Slice.id)).scalar() or 0
+        uri = f"api/v1/form_data/?slice_id={max_id + 10_000}"
+        rv = self.client.get(uri)
+        assert rv.status_code == 404
+
     @pytest.mark.usefixtures(
         "load_unicode_dashboard_with_slice",
         "load_energy_table_with_slice",