Apache Airflow: Bypass permission verification to read code of other dags
Description
Apache Airflow, versions before 2.8.1, have a vulnerability that allows an authenticated user to access the source code of a DAG to which they don't have access. This vulnerability is considered low since it requires an authenticated user to exploit it. Users are recommended to upgrade to version 2.8.1, which fixes this issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Authenticated users can bypass DAG-level access controls in Apache Airflow before 2.8.1 to read source code of DAGs they lack permission to view.
Vulnerability
Analysis
Apache Airflow versions prior to 2.8.1 contain an authorization bypass vulnerability in the DAG code access endpoint. An authenticated user with any valid session could request the source code of a DAG (Directed Acyclic Graph) even when the user lacked explicit read permission on that specific DAG [2]. The root cause was the absence of a permission check before serving the DAG's file contents via the API endpoint that returns DAG source code [4].
Exploitation
Conditions
Exploitation requires only that the attacker is an authenticated user of the Airflow instance. No special privileges or administrative access are needed [2]. The vulnerability affects the /api/v1/dagSources/ endpoint, which serializes the DAG file location and returns the raw source code. The missing authorization check meant that any authenticated user could enumerate and retrieve DAG source code, even for DAGs they should not be able to access [3][4].
Impact
The vulnerability exposes the business logic, credentials, and embedded connections inside DAG definitions to unauthorized users. While the CVSS score is low due to the authentication requirement, the disclosure of proprietary workflow logic and potential secrets (passwords, API keys, database URIs) within DAG code represents a significant confidentiality risk [1][2].
Mitigation
The issue is fixed in Airflow 2.8.1. The fix, implemented in commit 8d76538 [3], adds a permission check that verifies the user has READ action on the DAG Code resource for the specific DAG before returning the source. Users are strongly recommended to upgrade to version 2.8.1 or later [2]. No workarounds are documented for earlier versions.
- GitHub - apache/airflow: Apache Airflow - A platform to programmatically author, schedule, and monitor workflows
- NVD - CVE-2023-50944
- Check DAG read permission before accessing DAG code (#36257) · apache/airflow@8d76538
- Check DAG read permission before accessing DAG code by hussein-awala · Pull Request #36257 · apache/airflow
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.
| Package | Affected versions | Patched versions |
|---|---|---|
apache-airflowPyPI | < 2.8.1rc1 | 2.8.1rc1 |
Affected products
3- osv-coords2 versions
< 2.8.1+ 1 more
- (no CPE)range: < 2.8.1
- (no CPE)range: < 2.8.1rc1
- Apache Software Foundation/Apache Airflowv5Range: 0
Patches
18d76538d6e10Check DAG read permission before accessing DAG code (#36257)
3 files changed · +78 −13
airflow/api_connexion/endpoints/dag_source_endpoint.py+16 −3 modified@@ -17,25 +17,38 @@ from __future__ import annotations from http import HTTPStatus +from typing import TYPE_CHECKING from flask import Response, current_app, request from itsdangerous import BadSignature, URLSafeSerializer from airflow.api_connexion import security -from airflow.api_connexion.exceptions import NotFound +from airflow.api_connexion.exceptions import NotFound, PermissionDenied from airflow.api_connexion.schemas.dag_source_schema import dag_source_schema +from airflow.api_connexion.security import get_readable_dags from airflow.auth.managers.models.resource_details import DagAccessEntity +from airflow.models.dag import DagModel from airflow.models.dagcode import DagCode +from airflow.utils.session import NEW_SESSION, provide_session + +if TYPE_CHECKING: + from sqlalchemy.orm import Session @security.requires_access_dag("GET", DagAccessEntity.CODE) -def get_dag_source(*, file_token: str) -> Response: +@provide_session +def get_dag_source(*, file_token: str, session: Session = NEW_SESSION) -> Response: """Get source code using file token.""" secret_key = current_app.config["SECRET_KEY"] auth_s = URLSafeSerializer(secret_key) try: path = auth_s.loads(file_token) - dag_source = DagCode.code(path) + dag_ids = session.query(DagModel.dag_id).filter(DagModel.fileloc == path).all() + readable_dags = get_readable_dags() + # Check if user has read access to all the DAGs defined in the file + if any(dag_id[0] not in readable_dags for dag_id in dag_ids): + raise PermissionDenied() + dag_source = DagCode.code(path, session=session) except (BadSignature, FileNotFoundError): raise NotFound("Dag source not found")
airflow/models/dagcode.py+3 −2 modified@@ -177,12 +177,13 @@ def get_code_by_fileloc(cls, fileloc: str) -> str: return cls.code(fileloc) @classmethod - def code(cls, fileloc) -> str: + @provide_session + def code(cls, fileloc, session: Session = NEW_SESSION) -> str: """Return source code for this DagCode object. :return: source code as string """ - return cls._get_code_from_db(fileloc) + return cls._get_code_from_db(fileloc, session) @staticmethod def _get_code_from_file(fileloc):
tests/api_connexion/endpoints/test_dag_source_endpoint.py+59 −8 modified@@ -34,6 +34,10 @@ ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) EXAMPLE_DAG_FILE = os.path.join("airflow", "example_dags", "example_bash_operator.py") +EXAMPLE_DAG_ID = "example_bash_operator" +TEST_DAG_ID = "latest_only" +NOT_READABLE_DAG_ID = "latest_only_with_trigger" +TEST_MULTIPLE_DAGS_ID = "dataset_produces_1" @pytest.fixture(scope="module") @@ -45,6 +49,18 @@ def configured_app(minimal_app_for_api): role_name="Test", permissions=[(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_CODE)], # type: ignore ) + app.appbuilder.sm.sync_perm_for_dag( # type: ignore + TEST_DAG_ID, + access_control={"Test": [permissions.ACTION_CAN_READ]}, + ) + app.appbuilder.sm.sync_perm_for_dag( # type: ignore + EXAMPLE_DAG_ID, + access_control={"Test": [permissions.ACTION_CAN_READ]}, + ) + app.appbuilder.sm.sync_perm_for_dag( # type: ignore + TEST_MULTIPLE_DAGS_ID, + access_control={"Test": [permissions.ACTION_CAN_READ]}, + ) create_user(app, username="test_no_permissions", role_name="TestNoPermissions") # type: ignore yield app @@ -80,10 +96,10 @@ def _get_dag_file_docstring(fileloc: str) -> str | None: def test_should_respond_200_text(self, url_safe_serializer): dagbag = DagBag(dag_folder=EXAMPLE_DAG_FILE) dagbag.sync_to_db() - first_dag: DAG = next(iter(dagbag.dags.values())) - dag_docstring = self._get_dag_file_docstring(first_dag.fileloc) + test_dag: DAG = dagbag.dags[TEST_DAG_ID] + dag_docstring = self._get_dag_file_docstring(test_dag.fileloc) - url = f"/api/v1/dagSources/{url_safe_serializer.dumps(first_dag.fileloc)}" + url = f"/api/v1/dagSources/{url_safe_serializer.dumps(test_dag.fileloc)}" response = self.client.get( url, headers={"Accept": "text/plain"}, environ_overrides={"REMOTE_USER": "test"} ) @@ -95,10 +111,10 @@ def test_should_respond_200_text(self, url_safe_serializer): def test_should_respond_200_json(self, url_safe_serializer): dagbag = DagBag(dag_folder=EXAMPLE_DAG_FILE) dagbag.sync_to_db() - first_dag: DAG = next(iter(dagbag.dags.values())) - dag_docstring = self._get_dag_file_docstring(first_dag.fileloc) + test_dag: DAG = dagbag.dags[TEST_DAG_ID] + dag_docstring = self._get_dag_file_docstring(test_dag.fileloc) - url = f"/api/v1/dagSources/{url_safe_serializer.dumps(first_dag.fileloc)}" + url = f"/api/v1/dagSources/{url_safe_serializer.dumps(test_dag.fileloc)}" response = self.client.get( url, headers={"Accept": "application/json"}, environ_overrides={"REMOTE_USER": "test"} ) @@ -110,9 +126,9 @@ def test_should_respond_200_json(self, url_safe_serializer): def test_should_respond_406(self, url_safe_serializer): dagbag = DagBag(dag_folder=EXAMPLE_DAG_FILE) dagbag.sync_to_db() - first_dag: DAG = next(iter(dagbag.dags.values())) + test_dag: DAG = dagbag.dags[TEST_DAG_ID] - url = f"/api/v1/dagSources/{url_safe_serializer.dumps(first_dag.fileloc)}" + url = f"/api/v1/dagSources/{url_safe_serializer.dumps(test_dag.fileloc)}" response = self.client.get( url, headers={"Accept": "image/webp"}, environ_overrides={"REMOTE_USER": "test"} ) @@ -151,3 +167,38 @@ def test_should_raise_403_forbidden(self, url_safe_serializer): environ_overrides={"REMOTE_USER": "test_no_permissions"}, ) assert response.status_code == 403 + + def test_should_respond_403_not_readable(self, url_safe_serializer): + dagbag = DagBag(dag_folder=EXAMPLE_DAG_FILE) + dagbag.sync_to_db() + dag: DAG = dagbag.dags[NOT_READABLE_DAG_ID] + + response = self.client.get( + f"/api/v1/dagSources/{url_safe_serializer.dumps(dag.fileloc)}", + headers={"Accept": "text/plain"}, + environ_overrides={"REMOTE_USER": "test"}, + ) + read_dag = self.client.get( + f"/api/v1/dags/{NOT_READABLE_DAG_ID}", + environ_overrides={"REMOTE_USER": "test"}, + ) + assert response.status_code == 403 + assert read_dag.status_code == 403 + + def test_should_respond_403_some_dags_not_readable_in_the_file(self, url_safe_serializer): + dagbag = DagBag(dag_folder=EXAMPLE_DAG_FILE) + dagbag.sync_to_db() + dag: DAG = dagbag.dags[TEST_MULTIPLE_DAGS_ID] + + response = self.client.get( + f"/api/v1/dagSources/{url_safe_serializer.dumps(dag.fileloc)}", + headers={"Accept": "text/plain"}, + environ_overrides={"REMOTE_USER": "test"}, + ) + + read_dag = self.client.get( + f"/api/v1/dags/{TEST_MULTIPLE_DAGS_ID}", + environ_overrides={"REMOTE_USER": "test"}, + ) + assert response.status_code == 403 + assert read_dag.status_code == 200
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/apache/airflow/pull/36257ghsapatchWEB
- github.com/advisories/GHSA-vm5m-qmrx-fw8wghsaADVISORY
- lists.apache.org/thread/92krb5mpcq8qrw4t4j5oooqw7hgd8q7hghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2023-50944ghsaADVISORY
- www.openwall.com/lists/oss-security/2024/01/24/5ghsaWEB
- github.com/apache/airflow/commit/8d76538d6e105947272b000581c6fabec20146b1ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/apache-airflow/PYSEC-2024-14.yamlghsaWEB
News mentions
0No linked articles in our index yet.