VYPR
Moderate severityNVD Advisory· Published May 13, 2025· Updated Sep 1, 2025

Apache Superset: Incorrect authorization leading to resource ownership takeover

CVE-2025-27696

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.

PackageAffected versionsPatched versions
apache-supersetPyPI
< 4.1.24.1.2

Affected products

4

Patches

1
fc844d3dfdac

fix: dashboard, chart and dataset import validation (#32500)

https://github.com/apache/supersetDaniel Vaz GasparMar 5, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.