VYPR
Unrated severityNVD Advisory· Published Jun 1, 2026

CVE-2026-40963

CVE-2026-40963

Description

Airflow UI's structure_data endpoint leaks linked DAG IDs and dependency metadata for any DAG, even without read permission, enabling enumeration of unauthorized DAG topology.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Airflow UI's `structure_data` endpoint leaks linked DAG IDs and dependency metadata for any DAG, even without read permission, enabling enumeration of unauthorized DAG topology.

Vulnerability

The structure_data endpoint in the Airflow UI returns external dependency graph nodes for linked DAGs without verifying the caller has read permission on those linked DAGs [1]. Affected deployments are those using per-Dag read scoping to keep DAG dependency topology private across teams. Versions prior to apache-airflow 3.2.2 are vulnerable.

Exploitation

An authenticated UI/API user who has read access to at least one DAG can query the structure_data endpoint. The endpoint returns dependency metadata including linked DAG IDs for any other DAG, regardless of the user's authorization [1]. No additional privileges or user interaction beyond authentication to the Airflow instance is required.

Impact

An attacker can enumerate linked DAG IDs and dependency metadata for DAGs they are not authorized to read, violating confidentiality of DAG dependency topology [1]. This undermines per-Dag read scoping and can expose team-internal workflow structures.

Mitigation

Upgrade to apache-airflow 3.2.2 or later [1]. The fix adds the ReadableDagsFilterDep filter, which skips dependency entries that reference DAGs outside the caller's readable set [1]. No workaround is described for versions prior to 3.2.2; users are advised to upgrade.

AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

2
01888df3ae9b

Filter external dependency nodes by readable DAGs in structure_data endpoint (#65342)

https://github.com/apache/airflowJarek PotiukApr 20, 2026via nvd-ref
2 files changed · +37 6
  • airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py+13 1 modified
    @@ -26,7 +26,7 @@
     from airflow.api_fastapi.common.router import AirflowRouter
     from airflow.api_fastapi.core_api.datamodels.ui.structure import StructureDataResponse
     from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
    -from airflow.api_fastapi.core_api.security import requires_access_dag
    +from airflow.api_fastapi.core_api.security import ReadableDagsFilterDep, requires_access_dag
     from airflow.api_fastapi.core_api.services.ui.structure import (
         bind_output_assets_to_tasks,
         get_upstream_assets,
    @@ -52,6 +52,7 @@
     def structure_data(
         session: SessionDep,
         dag_id: str,
    +    readable_dags_filter: ReadableDagsFilterDep,
         include_upstream: QueryIncludeUpstream = False,
         include_downstream: QueryIncludeDownstream = False,
         depth: int | None = None,
    @@ -105,11 +106,22 @@ def structure_data(
             start_edges: list[dict] = []
             end_edges: list[dict] = []
     
    +        readable_dag_ids = readable_dags_filter.value
             for dependency_dag_id, dependencies in sorted(SerializedDagModel.get_dag_dependencies().items()):
    +            if readable_dag_ids is not None and dependency_dag_id not in readable_dag_ids:
    +                continue
                 for dependency in dependencies:
                     # Dependencies not related to `dag_id` are ignored
                     if dependency_dag_id != dag_id and dependency.target != dag_id:
                         continue
    +                # When target is a real DAG ID (not a type label), hide it
    +                # if the caller cannot read that DAG.
    +                if (
    +                    readable_dag_ids is not None
    +                    and dependency.target != dependency.dependency_type
    +                    and dependency.target not in readable_dag_ids
    +                ):
    +                    continue
     
                     # upstream assets are handled by the `get_upstream_assets` function.
                     if dependency.target != dependency.dependency_type and dependency.dependency_type in [
    
  • airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py+24 5 modified
    @@ -18,6 +18,7 @@
     from __future__ import annotations
     
     import copy
    +from unittest import mock
     
     import pendulum
     import pytest
    @@ -303,15 +304,15 @@ class TestStructureDataEndpoint:
                             },
                         ],
                     },
    -                6,
    +                7,
                 ),
                 (
                     {
                         "dag_id": DAG_ID,
                         "root": "unknown_task",
                     },
                     {"edges": [], "nodes": []},
    -                6,
    +                7,
                 ),
                 (
                     {
    @@ -336,7 +337,7 @@ class TestStructureDataEndpoint:
                             },
                         ],
                     },
    -                6,
    +                7,
                 ),
                 (
                     {"dag_id": DAG_ID_EXTERNAL_TRIGGER, "external_dependencies": True},
    @@ -375,7 +376,7 @@ class TestStructureDataEndpoint:
                             },
                         ],
                     },
    -                13,
    +                14,
                 ),
             ],
         )
    @@ -572,7 +573,7 @@ def test_should_return_200_with_asset(self, test_client, asset1_id, asset2_id, a
                 ],
             }
     
    -        with assert_queries_count(13):
    +        with assert_queries_count(14):
                 response = test_client.get("/structure/structure_data", params=params)
             assert response.status_code == 200
             assert response.json() == expected
    @@ -685,6 +686,24 @@ def test_delete_dag_should_response_403(self, unauthorized_test_client):
             response = unauthorized_test_client.get("/structure/structure_data", params={"dag_id": DAG_ID})
             assert response.status_code == 403
     
    +    @mock.patch(
    +        "airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids",
    +        return_value={DAG_ID_EXTERNAL_TRIGGER},
    +    )
    +    @pytest.mark.usefixtures("make_dags")
    +    def test_external_deps_filters_unreadable_dags(self, _, test_client):
    +        response = test_client.get(
    +            "/structure/structure_data",
    +            params={"dag_id": DAG_ID_EXTERNAL_TRIGGER, "external_dependencies": True},
    +        )
    +        assert response.status_code == 200
    +        result = response.json()
    +        node_ids = {node["id"] for node in result["nodes"]}
    +        assert "trigger_dag_run_operator" in node_ids
    +        assert not any(DAG_ID in nid for nid in node_ids if nid != "trigger_dag_run_operator")
    +        edge_targets = {edge["target_id"] for edge in result["edges"]}
    +        assert not any(DAG_ID in tid for tid in edge_targets)
    +
         def test_should_return_404(self, test_client):
             response = test_client.get("/structure/structure_data", params={"dag_id": "not_existing"})
             assert response.status_code == 404
    
cde4885818be

Updating release notes for 3.2.2rc3

https://github.com/apache/airflowvatsrahul1001May 26, 2026Fixed in 3.2.2via release-tag
2 files changed · +5 4
  • RELEASE_NOTES.rst+3 2 modified
    @@ -24,7 +24,7 @@
     
     .. towncrier release notes start
     
    -Airflow 3.2.2 (2026-05-27)
    +Airflow 3.2.2 (2026-05-29)
     --------------------------
     
     Significant Changes
    @@ -81,7 +81,8 @@ Significant Changes
     
     Bug Fixes
     ^^^^^^^^^
    -
    +- Fix ``Callback.handle_event`` triggerer crash when OpenTelemetry metrics receive dict typed tag values (#67527) (#67529)
    +- UI: Rewrite ``modulepreload hrefs`` to the api-server static path (#67548) (#67556)
     - Correctly pre-allocate ``external_executor_id`` with multiple executors on PostgreSQL (#67388) (#67458)
     - Return raw import-error stacktrace when a Dag file has no registered Dag (#67465) (#67478)
     - UI: Fix Expand/Collapse All on XComs and Audit Log JSON cells (#67316) (#67361)
    
  • reproducible_build.yaml+2 2 modified
    @@ -1,2 +1,2 @@
    -release-notes-hash: 6407b48d1054fe3ce68c09bf4435d91d
    -source-date-epoch: 1779745327
    +release-notes-hash: 504288db9a9dc13a0db859232fab98d0
    +source-date-epoch: 1779811737
    

Vulnerability mechanics

Root cause

"Missing authorization check in the structure_data endpoint allows an authenticated user to read external dependency graph nodes for DAGs they are not permitted to access."

Attack vector

An authenticated UI/API user who is authorized for one DAG can enumerate linked DAG IDs and dependency metadata for other DAGs they are not authorized to read. The attacker triggers the bug by calling the `structure_data` endpoint with `external_dependencies=True` for a DAG they can access; the endpoint returns dependency nodes for linked DAGs without verifying read permissions on those linked DAGs [CWE-285]. This affects deployments that rely on per-DAG read scoping to keep DAG dependency topology private across teams.

Affected code

The vulnerability is in the `structure_data` endpoint at `airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py`. The endpoint returned external dependency graph nodes for linked DAGs without checking whether the caller had read permission on those linked DAGs. The patch adds a `ReadableDagsFilterDep` dependency and filters out dependency entries that reference DAGs outside the caller's readable set.

What the fix does

The patch adds a `ReadableDagsFilterDep` dependency to the `structure_data` endpoint and uses it to skip dependency entries whose `dependency_dag_id` or `dependency.target` is not in the caller's readable DAG set. Specifically, it checks `readable_dag_ids is not None and dependency_dag_id not in readable_dag_ids` to skip entire dependency groups, and a second check to skip individual dependency entries whose target is a real DAG ID (not a type label) that the caller cannot read. This ensures that external dependency graph nodes are only returned for DAGs the caller has permission to view.

Preconditions

  • authThe attacker must be an authenticated user of the Airflow UI/API with read access to at least one DAG.
  • configThe deployment must use per-DAG read scoping (i.e., not all DAGs are readable by all users).
  • inputThe attacker sends a GET request to the /structure/structure_data endpoint with external_dependencies=True for a DAG they can access.

Generated on Jun 1, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.