Skip to content

Commit 335c10b

Browse files
betodealmeidasfirke
authored andcommitted
fix: prevent guest user from modifying metrics (apache#26749)
1 parent b39449b commit 335c10b

File tree

4 files changed

+117
-2
lines changed

4 files changed

+117
-2
lines changed

superset/common/query_object.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def __init__( # pylint: disable=too-many-locals
125125
order_desc: bool = True,
126126
orderby: list[OrderBy] | None = None,
127127
post_processing: list[dict[str, Any] | None] | None = None,
128-
row_limit: int | None,
128+
row_limit: int | None = None,
129129
row_offset: int | None = None,
130130
series_columns: list[Column] | None = None,
131131
series_limit: int = 0,

superset/security/manager.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -1819,7 +1819,7 @@ def get_exclude_users_from_lists() -> list[str]:
18191819
return []
18201820

18211821
def raise_for_access(
1822-
# pylint: disable=too-many-arguments,too-many-branches,too-many-locals
1822+
# pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements
18231823
self,
18241824
dashboard: Optional["Dashboard"] = None,
18251825
database: Optional["Database"] = None,
@@ -1909,6 +1909,30 @@ def raise_for_access(
19091909
self.get_table_access_error_object(denied)
19101910
)
19111911

1912+
if self.is_guest_user() and query_context:
1913+
# Guest users MUST not modify the payload so it's requesting a different
1914+
# chart or different ad-hoc metrics from what's saved.
1915+
form_data = query_context.form_data
1916+
stored_chart = query_context.slice_
1917+
1918+
if (
1919+
form_data is None
1920+
or stored_chart is None
1921+
or form_data.get("slice_id") != stored_chart.id
1922+
or form_data.get("metrics", []) != stored_chart.params_dict["metrics"]
1923+
or any(
1924+
query.metrics != stored_chart.params_dict["metrics"]
1925+
for query in query_context.queries
1926+
)
1927+
):
1928+
raise SupersetSecurityException(
1929+
SupersetError(
1930+
error_type=SupersetErrorType.DASHBOARD_SECURITY_ACCESS_ERROR,
1931+
message=_("Guest user cannot modify chart payload"),
1932+
level=ErrorLevel.ERROR,
1933+
)
1934+
)
1935+
19121936
if datasource or query_context or viz:
19131937
form_data = None
19141938

tests/integration_tests/security/guest_token_security_tests.py

+15
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,10 @@ def test_raise_for_access__happy_path(self):
288288
form_data={
289289
"dashboardId": self.dash.id,
290290
"slice_id": self.chart.id,
291+
"metrics": self.chart.params_dict["metrics"],
291292
},
293+
slice_=self.chart,
294+
queries=[],
292295
)
293296
}
294297
)
@@ -304,7 +307,11 @@ def test_raise_for_access__native_filter_happy_path(self):
304307
"dashboardId": self.dash.id,
305308
"native_filter_id": "NATIVE_FILTER-ABCDEFGH",
306309
"type": "NATIVE_FILTER",
310+
"slice_id": self.chart.id,
311+
"metrics": self.chart.params_dict["metrics"],
307312
},
313+
slice_=self.chart,
314+
queries=[],
308315
)
309316
}
310317
)
@@ -382,7 +389,11 @@ def test_raise_for_access__native_filter_no_id_in_form_data(self):
382389
form_data={
383390
"dashboardId": self.dash.id,
384391
"type": "NATIVE_FILTER",
392+
"slice_id": self.chart.id,
393+
"metrics": self.chart.params_dict["metrics"],
385394
},
395+
slice_=self.chart,
396+
queries=[],
386397
)
387398
}
388399
)
@@ -399,7 +410,11 @@ def test_raise_for_access__native_filter_datasource_not_associated(self):
399410
"dashboardId": self.dash.id,
400411
"native_filter_id": "NATIVE_FILTER-ABCDEFGH",
401412
"type": "NATIVE_FILTER",
413+
"slice_id": self.chart.id,
414+
"metrics": self.chart.params_dict["metrics"],
402415
},
416+
slice_=self.chart,
417+
queries=[],
403418
)
404419
}
405420
)

tests/unit_tests/security/manager_test.py

+76
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import pytest
1919
from pytest_mock import MockFixture
2020

21+
from superset.common.query_object import QueryObject
2122
from superset.exceptions import SupersetSecurityException
2223
from superset.extensions import appbuilder
2324
from superset.security.manager import SupersetSecurityManager
@@ -31,6 +32,81 @@ def test_security_manager(app_context: None) -> None:
3132
assert sm
3233

3334

35+
def test_raise_for_access_guest_user(
36+
mocker: MockFixture,
37+
app_context: None,
38+
) -> None:
39+
"""
40+
Test that guest user can't modify chart payload.
41+
"""
42+
sm = SupersetSecurityManager(appbuilder)
43+
mocker.patch.object(sm, "is_guest_user", return_value=True)
44+
mocker.patch.object(sm, "can_access", return_value=True)
45+
46+
query_context = mocker.MagicMock()
47+
query_context.slice_.id = 42
48+
stored_metrics = [
49+
{
50+
"aggregate": None,
51+
"column": None,
52+
"datasourceWarning": False,
53+
"expressionType": "SQL",
54+
"hasCustomLabel": False,
55+
"label": "COUNT(*) + 1",
56+
"optionName": "metric_ssa1gwimio_cxpyjc7vj3s",
57+
"sqlExpression": "COUNT(*) + 1",
58+
}
59+
]
60+
query_context.slice_.params_dict = {
61+
"metrics": stored_metrics,
62+
}
63+
64+
# normal request
65+
query_context.form_data = {
66+
"slice_id": 42,
67+
"metrics": stored_metrics,
68+
}
69+
query_context.queries = [QueryObject(metrics=stored_metrics)] # type: ignore
70+
sm.raise_for_access(query_context=query_context)
71+
72+
# tampered requests
73+
query_context.form_data = {
74+
"slice_id": 43,
75+
"metrics": stored_metrics,
76+
}
77+
query_context.queries = [QueryObject(metrics=stored_metrics)] # type: ignore
78+
with pytest.raises(SupersetSecurityException):
79+
sm.raise_for_access(query_context=query_context)
80+
81+
tampered_metrics = [
82+
{
83+
"aggregate": None,
84+
"column": None,
85+
"datasourceWarning": False,
86+
"expressionType": "SQL",
87+
"hasCustomLabel": False,
88+
"label": "COUNT(*) + 2",
89+
"optionName": "metric_ssa1gwimio_cxpyjc7vj3s",
90+
"sqlExpression": "COUNT(*) + 2",
91+
}
92+
]
93+
94+
query_context.form_data = {
95+
"slice_id": 42,
96+
"metrics": tampered_metrics,
97+
}
98+
with pytest.raises(SupersetSecurityException):
99+
sm.raise_for_access(query_context=query_context)
100+
101+
query_context.form_data = {
102+
"slice_id": 42,
103+
"metrics": stored_metrics,
104+
}
105+
query_context.queries = [QueryObject(metrics=tampered_metrics)] # type: ignore
106+
with pytest.raises(SupersetSecurityException):
107+
sm.raise_for_access(query_context=query_context)
108+
109+
34110
def test_raise_for_access_query_default_schema(
35111
mocker: MockFixture,
36112
app_context: None,

0 commit comments

Comments
 (0)