Apache Superset: Incorrect authorization leading to resource ownership takeover
Description
Incorrect Authorization vulnerability in Apache Superset allows ownership takeover of dashboards, charts or datasets by authenticated users with read permissions.
This issue affects Apache Superset: through 4.1.1.
Users are recommended to upgrade to version 4.1.2 or above, which fixes the issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Authenticated users with read permissions in Apache Superset could overwrite and steal ownership of dashboards, charts, or datasets before 4.1.2.
Vulnerability
Overview
CVE-2025-27696 is an Incorrect Authorization vulnerability in Apache Superset that allows an authenticated user with only read permissions to take ownership of dashboards, charts, or datasets [1][2][4]. The root cause lies in the import functionality for these resources, where the validation logic failed to properly check if the importing user is among the existing owners or has administrative privileges before overwriting the resource [1].
Exploitation
Prerequisites
An attacker must have a valid authenticated session with at least read-level access to Apache Superset. The attack is performed via the import API endpoints for dashboards, charts, and datasets. The patched code shows that the fix adds checks ensuring the user is either an existing owner or an admin; otherwise, the import is rejected with an error message [1]. Prior to the fix, the overwrite path could succeed without verifying the user's authorization to alter ownership.
Impact
An authenticated user who does not own the targeted resource can successfully overwrite it during an import operation, thereby becoming the new owner. This effectively allows ownership takeover of dashboards, charts, or datasets that the attacker can read but should not be able to write to or transfer [2][4]. The impact undermines the access control model of Apache Superset, potentially leading to unauthorized data manipulation or exposure.
Mitigation
The vulnerability affects Apache Superset through version 4.1.1. Users must upgrade to version 4.1.2 or later, which contains the commit that enforces proper ownership and admin checks during import [1][2]. No workaround is mentioned; upgrading is the recommended action [4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
apache-supersetPyPI | < 4.1.2 | 4.1.2 |
Affected products
4- osv-coords2 versions
< 4.1.2+ 1 more
- (no CPE)range: < 4.1.2
- (no CPE)range: < 4.1.2
- Apache Software Foundation/Apache Supersetv5Range: 0
Patches
1fc844d3dfdacfix: dashboard, chart and dataset import validation (#32500)
6 files changed · +171 −7
superset/commands/chart/importers/v1/utils.py+5 −2 modified@@ -50,9 +50,12 @@ def import_chart( ) -> Slice: can_write = ignore_permissions or security_manager.can_access("can_write", "Chart") existing = db.session.query(Slice).filter_by(uuid=config["uuid"]).first() + user = get_user() if existing: - if overwrite and can_write and get_user(): - if not security_manager.can_access_chart(existing): + if overwrite and can_write and user: + if not security_manager.can_access_chart(existing) or ( + user not in existing.owners and not security_manager.is_admin() + ): raise ImportFailedError( "A chart already exists and user doesn't " "have permissions to overwrite it"
superset/commands/dashboard/importers/v1/utils.py+5 −2 modified@@ -153,9 +153,12 @@ def import_dashboard( # noqa: C901 "Dashboard", ) existing = db.session.query(Dashboard).filter_by(uuid=config["uuid"]).first() + user = get_user() if existing: - if overwrite and can_write and get_user(): - if not security_manager.can_access_dashboard(existing): + if overwrite and can_write and user: + if not security_manager.can_access_dashboard(existing) or ( + user not in existing.owners and not security_manager.is_admin() + ): raise ImportFailedError( "A dashboard already exists and user doesn't " "have permissions to overwrite it"
superset/commands/dataset/importers/v1/utils.py+8 −0 modified@@ -113,10 +113,18 @@ def import_dataset( # noqa: C901 "Dataset", ) existing = db.session.query(SqlaTable).filter_by(uuid=config["uuid"]).first() + user = get_user() if existing: + if overwrite and can_write and user: + if user not in existing.owners and not security_manager.is_admin(): + raise ImportFailedError( + "A dataset already exists and user doesn't " + "have permissions to overwrite it" + ) if not overwrite or not can_write: return existing config["id"] = existing.id + elif not can_write: raise ImportFailedError( "Dataset doesn't exist and user doesn't have permission to create datasets"
tests/unit_tests/charts/commands/importers/v1/import_test.py+50 −1 modified@@ -181,7 +181,56 @@ def test_import_existing_chart_without_permission( .one_or_none() ) - with override_user("admin"): + user = User( + first_name="Alice", + last_name="Doe", + email="adoe@example.org", + username="admin", + roles=[Role(name="Admin")], + ) + + with override_user(user): + with pytest.raises(ImportFailedError) as excinfo: + import_chart(chart_config, overwrite=True) + assert ( + str(excinfo.value) + == "A chart already exists and user doesn't have permissions to overwrite it" # noqa: E501 + ) + + # Assert that the can write to chart was checked + mock_can_access.assert_called_once_with("can_write", "Chart") + mock_can_access_chart.assert_called_once_with(slice) + + +def test_import_existing_chart_without_owner_permission( + mocker: MockerFixture, + session_with_data: Session, +) -> None: + """ + Test importing a chart when a user doesn't have permissions to modify. + """ + mock_can_access = mocker.patch.object( + security_manager, "can_access", return_value=True + ) + mock_can_access_chart = mocker.patch.object( + security_manager, "can_access_chart", return_value=True + ) + + slice = ( + session_with_data.query(Slice) + .filter(Slice.uuid == chart_config["uuid"]) + .one_or_none() + ) + + user = User( + first_name="Alice", + last_name="Doe", + email="adoe@example.org", + username="admin", + roles=[Role(name="Gamma")], + ) + + with override_user(user): with pytest.raises(ImportFailedError) as excinfo: import_chart(chart_config, overwrite=True) assert (
tests/unit_tests/dashboards/commands/importers/v1/import_test.py+51 −2 modified@@ -122,7 +122,7 @@ def test_import_dashboard_without_permission( mock_can_access.assert_called_once_with("can_write", "Dashboard") -def test_import_existing_dashboard_without_permission( +def test_import_existing_dashboard_without_access_permission( mocker: MockerFixture, session_with_data: Session, ) -> None: @@ -142,7 +142,56 @@ def test_import_existing_dashboard_without_permission( .one_or_none() ) - with override_user("admin"): + admin = User( + first_name="Alice", + last_name="Doe", + email="adoe@example.org", + username="admin", + roles=[Role(name="Admin")], + ) + + with override_user(admin): + with pytest.raises(ImportFailedError) as excinfo: + import_dashboard(dashboard_config, overwrite=True) + assert ( + str(excinfo.value) + == "A dashboard already exists and user doesn't have permissions to overwrite it" # noqa: E501 + ) + + # Assert that the can write to dashboard was checked + mock_can_access.assert_called_once_with("can_write", "Dashboard") + mock_can_access_dashboard.assert_called_once_with(dashboard) + + +def test_import_existing_dashboard_without_owner_permission( + mocker: MockerFixture, + session_with_data: Session, +) -> None: + """ + Test importing a dashboard when a user doesn't have ownership and is not an Admin. + """ + mock_can_access = mocker.patch.object( + security_manager, "can_access", return_value=True + ) + mock_can_access_dashboard = mocker.patch.object( + security_manager, "can_access_dashboard", return_value=True + ) + + dashboard = ( + session_with_data.query(Dashboard) + .filter(Dashboard.uuid == dashboard_config["uuid"]) + .one_or_none() + ) + + user = User( + first_name="Alice", + last_name="Doe", + email="adoe@example.org", + username="admin", + roles=[Role(name="Gamma")], + ) + + with override_user(user): with pytest.raises(ImportFailedError) as excinfo: import_dashboard(dashboard_config, overwrite=True) assert (
tests/unit_tests/datasets/commands/importers/v1/import_test.py+52 −0 modified@@ -24,6 +24,7 @@ import pytest from flask import current_app +from flask_appbuilder.security.sqla.models import Role, User from pytest_mock import MockerFixture from sqlalchemy.orm.session import Session @@ -32,7 +33,9 @@ DatasetForbiddenDataURI, ) from superset.commands.dataset.importers.v1.utils import validate_data_uri +from superset.commands.exceptions import ImportFailedError from superset.utils import json +from superset.utils.core import override_user def test_import_dataset(mocker: MockerFixture, session: Session) -> None: @@ -536,6 +539,55 @@ def test_import_dataset_managed_externally( assert sqla_table.external_url == "https://example.org/my_table" +def test_import_dataset_without_owner_permission( + mocker: MockerFixture, + session: Session, +) -> None: + """ + Test importing a dataset that is managed externally. + """ + from superset import security_manager + from superset.commands.dataset.importers.v1.utils import import_dataset + from superset.connectors.sqla.models import SqlaTable + from superset.models.core import Database + from tests.integration_tests.fixtures.importexport import dataset_config + + mock_can_access = mocker.patch.object( + security_manager, "can_access", return_value=True + ) + + engine = db.session.get_bind() + SqlaTable.metadata.create_all(engine) # pylint: disable=no-member + + database = Database(database_name="my_database", sqlalchemy_uri="sqlite://") + db.session.add(database) + db.session.flush() + + config = copy.deepcopy(dataset_config) + config["database_id"] = database.id + + import_dataset(config) + user = User( + first_name="Alice", + last_name="Doe", + email="adoe@example.org", + username="admin", + roles=[Role(name="Gamma")], + ) + + with override_user(user): + with pytest.raises(ImportFailedError) as excinfo: + import_dataset(config, overwrite=True) + + assert ( + str(excinfo.value) + == "A dataset already exists and user doesn't have permissions to overwrite it" # noqa: E501 + ) + + # Assert that the can write to chart was checked + mock_can_access.assert_called_with("can_write", "Dataset") + + @pytest.mark.parametrize( "allowed_urls, data_uri, expected, exception_class", [
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-w6c7-j32f-rq8jghsaADVISORY
- lists.apache.org/thread/k2od03bxnxs6vcp80sr03ywcxl194413ghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-27696ghsaADVISORY
- www.openwall.com/lists/oss-security/2025/05/12/3ghsaWEB
- github.com/apache/superset/commit/fc844d3dfdace890b32c00a507a959b81122b425ghsaWEB
News mentions
0No linked articles in our index yet.