VYPR
Unrated severityNVD Advisory· Published Jun 1, 2026

CVE-2026-41014

CVE-2026-41014

Description

CVE-2026-41014: Airflow's partitioned_dag_runs endpoints lack per-DAG authorization, allowing authenticated users with Asset:read to enumerate unauthorized DAG run data.

AI Insight

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

CVE-2026-41014: Airflow's partitioned_dag_runs endpoints lack per-DAG authorization, allowing authenticated users with Asset:read to enumerate unauthorized DAG run data.

Vulnerability

The vulnerability resides in the partitioned_dag_runs endpoints in the Airflow UI. These endpoints enforced only asset-level access control, not per-DAG authorization. An authenticated user with global Asset:read permission could access partition run state, schedule configuration, and asset wiring for DAGs they were not authorized to read. Affected versions are those prior to apache-airflow 3.2.2.

Exploitation

An attacker needs to be an authenticated UI/API user with at least Asset:read permission. No additional privileges are required. The attacker can directly query the partitioned_dag_runs endpoints to enumerate information about DAGs they should not have access to. The steps involve authenticating and sending requests to the vulnerable endpoints to retrieve data for unauthorized DAGs.

Impact

Successful exploitation leads to unauthorized information disclosure. The attacker gains access to partition run state, schedule configuration, and asset wiring details for DAGs they are not permitted to read. This violates the intended per-DAG read scoping and can expose sensitive operational data.

Mitigation

The fix is included in apache-airflow 3.2.2 or later. Users should upgrade to that version. The fix adds requires_access_dag and ReadableDagsFilterDep to the endpoints, matching the pattern used by the sibling next_run_assets endpoint [1]. No workaround is mentioned; upgrading is the recommended action.

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
e36d10a5196d

Add per-DAG authorization to partitioned_dag_runs endpoints (#65344)

https://github.com/apache/airflowJarek PotiukApr 20, 2026via nvd-ref
2 files changed · +32 4
  • airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py+10 2 modified
    @@ -31,7 +31,11 @@
         PartitionedDagRunDetailResponse,
         PartitionedDagRunResponse,
     )
    -from airflow.api_fastapi.core_api.security import requires_access_asset
    +from airflow.api_fastapi.core_api.security import (
    +    ReadableDagsFilterDep,
    +    requires_access_asset,
    +    requires_access_dag,
    +)
     from airflow.models import DagModel
     from airflow.models.asset import (
         AssetModel,
    @@ -63,6 +67,7 @@ def _build_response(row, required_count: int) -> PartitionedDagRunResponse:
     )
     def get_partitioned_dag_runs(
         session: SessionDep,
    +    readable_dags_filter: ReadableDagsFilterDep,
         dag_id: QueryPartitionedDagRunDagIdFilter,
         has_created_dag_run_id: QueryPartitionedDagRunHasCreatedDagRunIdFilter,
     ) -> PartitionedDagRunCollectionResponse:
    @@ -123,6 +128,9 @@ def get_partitioned_dag_runs(
             received_subq.label("total_received"),
         ).outerjoin(DagRun, AssetPartitionDagRun.created_dag_run_id == DagRun.id)
         query = apply_filters_to_select(statement=query, filters=[dag_id, has_created_dag_run_id])
    +    readable_dag_ids = readable_dags_filter.value
    +    if readable_dag_ids is not None:
    +        query = query.where(AssetPartitionDagRun.target_dag_id.in_(readable_dag_ids))
         query = query.order_by(AssetPartitionDagRun.created_at.desc())
     
         if not (rows := session.execute(query).all()):
    @@ -162,7 +170,7 @@ def get_partitioned_dag_runs(
     
     @partitioned_dag_runs_router.get(
         "/pending_partitioned_dag_run/{dag_id}/{partition_key}",
    -    dependencies=[Depends(requires_access_asset(method="GET"))],
    +    dependencies=[Depends(requires_access_asset(method="GET")), Depends(requires_access_dag(method="GET"))],
     )
     def get_pending_partitioned_dag_run(
         dag_id: str,
    
  • airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py+22 2 modified
    @@ -16,6 +16,8 @@
     # under the License.
     from __future__ import annotations
     
    +from unittest import mock
    +
     import pendulum
     import pytest
     from sqlalchemy import select
    @@ -57,7 +59,7 @@ def test_should_response_200_non_partitioned_dag_returns_empty(self, test_client
             dag_maker.create_dagrun()
             dag_maker.sync_dagbag_to_db()
     
    -        with assert_queries_count(2):
    +        with assert_queries_count(3):
                 resp = test_client.get("/partitioned_dag_runs?dag_id=normal&has_created_dag_run_id=false")
             assert resp.status_code == 200
             assert resp.json() == {"partitioned_dag_runs": [], "total": 0, "asset_expressions": None}
    @@ -144,7 +146,7 @@ def test_should_response_200(
                 )
             session.commit()
     
    -        with assert_queries_count(2):
    +        with assert_queries_count(3):
                 resp = test_client.get(
                     f"/partitioned_dag_runs?dag_id=list_dag"
                     f"&has_created_dag_run_id={str(has_created_dag_run_id).lower()}"
    @@ -218,6 +220,24 @@ def _make_schedule(prefix, count):
             assert pdr_resp["total_required"] == num_target_assets
             assert pdr_resp["total_received"] == received_count
     
    +    @mock.patch(
    +        "airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids",
    +        return_value={"other_dag"},
    +    )
    +    def test_partitioned_dag_runs_filters_unreadable_dags(self, _, test_client, dag_maker, session):
    +        schedule = PartitionedAssetTimetable(assets=Asset(uri="s3://bucket/a", name="a"))
    +        with dag_maker(dag_id="restricted_dag", schedule=schedule, serialized=True):
    +            EmptyOperator(task_id="t")
    +        dag_maker.sync_dagbag_to_db()
    +        session.add(AssetPartitionDagRun(target_dag_id="restricted_dag", partition_key="2024-06-01"))
    +        session.commit()
    +
    +        resp = test_client.get("/partitioned_dag_runs?has_created_dag_run_id=false")
    +        assert resp.status_code == 200
    +        body = resp.json()
    +        dag_ids = {r["dag_id"] for r in body["partitioned_dag_runs"]}
    +        assert "restricted_dag" not in dag_ids
    +
     
     class TestGetPendingPartitionedDagRun:
         def test_should_response_401(self, unauthenticated_test_client):
    
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 per-DAG authorization check in the partitioned_dag_runs endpoints allowed users with only asset-level access to enumerate partition data for unauthorized DAGs."

Attack vector

An authenticated UI/API user with global `Asset:read` permission can call the `partitioned_dag_runs` or `pending_partitioned_dag_run` endpoints without per-DAG authorization. Because the endpoints only enforced asset-level access control [CWE-862], the attacker can enumerate partition run state, schedule configuration, and asset wiring for DAGs they are not authorized to read. The attack requires no special network position beyond authenticated access to the Airflow UI or API.

Affected code

The `get_partitioned_dag_runs` and `get_pending_partitioned_dag_run` endpoints in `airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py` lacked per-DAG authorization. The patch adds `ReadableDagsFilterDep` and `requires_access_dag` dependencies to these endpoints, mirroring the pattern already used by the sibling `next_run_assets` endpoint in `assets.py`.

What the fix does

The patch injects `ReadableDagsFilterDep` into `get_partitioned_dag_runs` and adds a `requires_access_dag(method="GET")` dependency to `get_pending_partitioned_dag_run`. The `ReadableDagsFilterDep` filters the query to only include rows whose `target_dag_id` is in the set of DAG IDs the current user is authorized to read. This closes the authorization gap by ensuring that even if a user holds `Asset:read`, they cannot access partition data for DAGs they are not permitted to see.

Preconditions

  • authThe user must be authenticated to the Airflow UI or API.
  • authThe user must hold global `Asset:read` permission.
  • configThe deployment must rely on per-DAG read scoping while granting broader Asset 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.