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
1Patches
2e36d10a5196dAdd per-DAG authorization to partitioned_dag_runs endpoints (#65344)
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):
cde4885818beUpdating release notes for 3.2.2rc3
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
2News mentions
0No linked articles in our index yet.