Intro

Greetings to all hackers out there! Today, I’ll shed light on a vulnerability I discovered in Apache Superset in late 2024. This journey was sparked at Bsides Lisbon 2024, where my interactions with seasoned experts inspired me to delve into researching open-source projects.

Upon my return, I aimed my investigation at Apache Superset, a tool I was familiar with from a previous DevOps role. At the time, I label it as an extremely potent and challenging tool with good security controls.

So… what is Superset?

To set the stage, Apache Superset is a versatile tool that provides data visualization and exploration capabilities, connects with a wide range of database types, and enables data representation via dashboards. These dashboards are constructed using charts, which essentially provide a visualization of the query results derived from the databases.

Example:

Interesting functionality

Having set up a container environment to test, I began exploring Superset’s functionalities. Among the capabilities tested, one feature in Superset caught my attention: the export and import of dashboards.

Before revealing the behind-the-scenes, here’s a glimpse of what you might see when exporting a sample dashboard:

As indicated by the snapshots above, the output is a zip file, housing a yaml file with metadata, backed by the charts, datasets, and databases that shape up the dashboard. The metadata of the dashboard component is intriguing, replete with numerous fields, including a UUID:

With no modifications made to the file fields, I imported the zip file as is, to observe the tool’s behaviour.

Exporting and Importing Dashboards? What can go wrong?

Upon initiating an import of the same zip file we just exported, an interesting message popped up:

Seemingly sensible, as the same dashboard was being imported again, I decided to select “OVERWRITE” and observed the aftermath.

Strange…. Nobody was the owner and I’m now the owner of the dashboard??? This odd behaviour implied the presence of an inconsistency in the dashboard ownership validation mechanism. To probe further, I created another user, assigned the permissions to export, import and view dashboards, and then imported a dashboard.

As evident from the video above, the platform allowed us to seize dashboard ownership, empowering us to edit it and dismiss the other owners, despite never owning it initially.

All dashboards?

Following this discovery, I was convinced that all dashboards could be overwritten. However, upon an attempt to create a new dashboard with another user, I could neither list it on the dashboards page nor export it via the API.

Note: The API endpoint used, GET /api/v1/dashboard/export/?q=!(dashboard_id), dashboard_id is incremental

The obstacle was due to the dashboard being in draft state. Transitioning it to a published state allowed me to export, import, and thus own it.

Here’s a request of a non-published dashboard:

Contrastingly, this is a published dashboard:

Now, one might ask: can we only overtake published dashboards?

While it’s true for the most part, there’s a slight twist. Remember the uuid in the dashboard metadata? Superset is looking at that uuid and is checking for it in the database, if it gets an entry it will overwrite the owners even if the dashboards are not published. However bruteforcing a UUID is very difficult and because of that it prevents attackers from importing a custom zip with the dashboard UUID if we lack the UUID upfront. Bellow is a demonstration of exporting and import a draft dashboard:

What about the other components in the exported zip?

Wouldn’t we require all the charts/datasets/databases linked to a specific dashboard in our custom-built zip instead of having just the dashboard’s UUID? No! What’s interesting is if a Superset dashboard comprises multiple resources tied to it—and we overwrite it with an empty dashboard holding only the UUID—all the resources remain as is in superset, and we become one of the owners. This puts us in power just by knowing the UUID and allows us to take over while preserving the same resources.

In the following example we create a dashboard with user 2 and some charts. We export and get uuid. Then as user 1 we will create an empty dashboard and export it. Change the uuid in the empty dashboard to the one from the user 2 because we want to overtake it. As you can see we can, just by knowing the uuid we can overtake and keep the same resources.

The source code gives all the answers

The main code responsible for dashboard import, found at superset/commands/importers/v1/assets.py, doesn’t perform the actual validation. It employs the import_dashboard function found at superset/commands/dashboard/importers/v1/assets.py, which unveils the vulnerability:

  1. Initially, they verify that we have the “can_write” and “Dashboard” permissions.
  2. They query the database for the UUID.
  3. If the dashboard exists, they check whether we possess the required permissions to overwrite it.
def import_dashboard(
    session: Session,
    config: dict[str, Any],
    overwrite: bool = False,
    ignore_permissions: bool = False,
) -> Dashboard:
    can_write = ignore_permissions or security_manager.can_access(
        "can_write",
        "Dashboard",
    )
    existing = session.query(Dashboard).filter_by(uuid=config["uuid"]).first()
    if existing:
        if overwrite and can_write and hasattr(g, "user") and g.user:
            if not security_manager.can_access_dashboard(existing):
                raise ImportFailedError(
                    "A dashboard already exists and user doesn't "
                    "have permissions to overwrite it"
                )
        elif not overwrite or not can_write:
            return existing
        config["id"] = existing.id
    elif not can_write:
        raise ImportFailedError(
            "Dashboard doesn't exist and user doesn't "
            "have permission to create dashboards"
        )

    # TODO (betodealmeida): move this logic to import_from_dict
    config = config.copy()

    # removed in https://github.com/apache/superset/pull/23228
    if "metadata" in config and "show_native_filters" in config["metadata"]:
        del config["metadata"]["show_native_filters"]

    for key, new_name in JSON_KEYS.items():
        if config.get(key) is not None:
            value = config.pop(key)
            try:
                config[new_name] = json.dumps(value)
            except TypeError:
                logger.info("Unable to encode `%s` field: %s", key, value)

    dashboard = Dashboard.import_from_dict(session, config, recursive=False)
    if dashboard.id is None:
        session.flush()

    if hasattr(g, "user") and g.user:
        dashboard.owners.append(g.user)

    return dashboard

Did you catch the problem? There’s no validation to check if we’re the actual owners. In the end, the system merely appends our user to the owners!

Only Dashboards? What about the other resources like datasets and charts?!

Yeah, you guessed it right – These resources have the import and export features too. But guess what? They don’t have a draft or publish state, meaning we can simply seize everything. Indeed, just by exporting and importing, we become owners of all asset types!

Conclusion

Vulnerabilities aren’t always wormholes hidden deep within complex payloads. Occasionally, they lay bare before our eyes, waiting for us to stray from orthodox methods and employ the easiest tricks. This vulnerability illustrated that, loud and clear. And there’s more to come with regards to Superset’s security–so stay tuned!

Fun fact: During my report of this vulnerability, the Apache security team informed me of a previous vulnerability revolving around the import function, causing me to triple-check if I was indeed testing the latest version. Although confident, the existing vulnerability seemed so apparent that it put me into a doubt spiral about the version I was testing. In case you’re curious, here’s the previously detected vulnerability: https://www.cve.org/CVERecord?id=CVE-2024-26016

Until next time. Keep hacking the planet! Link to the cve: https://www.cve.org/CVERecord?id=CVE-2025-27696 and https://nvd.nist.gov/vuln/detail/CVE-2025-27696