This is an automated email from the ASF dual-hosted git repository.
amoghrajesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 91cce6484ce Escape URL for DagOperations lookup in task sdk client
(#68129)
91cce6484ce is described below
commit 91cce6484cee216e16cae77fd14c33dfad7398e8
Author: GPK <[email protected]>
AuthorDate: Mon Jun 15 12:09:02 2026 +0100
Escape URL for DagOperations lookup in task sdk client (#68129)
---
task-sdk/src/airflow/sdk/api/client.py | 3 ++-
task-sdk/tests/task_sdk/api/test_client.py | 31 ++++++++++++++++++++++++++++++
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/task-sdk/src/airflow/sdk/api/client.py
b/task-sdk/src/airflow/sdk/api/client.py
index 824b0d27dc7..66496d5cac9 100644
--- a/task-sdk/src/airflow/sdk/api/client.py
+++ b/task-sdk/src/airflow/sdk/api/client.py
@@ -25,6 +25,7 @@ from datetime import datetime
from functools import cache
from http import HTTPStatus
from typing import TYPE_CHECKING, Any, TypeVar
+from urllib.parse import quote
import certifi
import httpx
@@ -989,7 +990,7 @@ class DagsOperations:
def get(self, dag_id: str) -> DagResponse:
"""Get a DAG via the API server."""
- resp = self.client.get(f"dags/{dag_id}")
+ resp = self.client.get(f"dags/{quote(dag_id, safe='')}")
return DagResponse.model_validate_json(resp.read())
diff --git a/task-sdk/tests/task_sdk/api/test_client.py
b/task-sdk/tests/task_sdk/api/test_client.py
index 2aeb0fc298a..0d6ee44628a 100644
--- a/task-sdk/tests/task_sdk/api/test_client.py
+++ b/task-sdk/tests/task_sdk/api/test_client.py
@@ -1737,6 +1737,37 @@ class TestDagsOperations:
next_dagrun=datetime(2026, 4, 13, tzinfo=dt_timezone.utc),
)
+ def test_get_url_quotes_dag_id_as_single_path_segment(self):
+ """Test that Dag IDs cannot escape the dags API path."""
+ requests_seen = []
+ crafted_dag_id = "x/../../variables/secret_key"
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ requests_seen.append(request)
+ if request.url.path == "/variables/secret_key":
+ return httpx.Response(
+ status_code=200,
+ json={"key": "secret_key", "value": "super-secret-value"},
+ )
+ return httpx.Response(
+ status_code=404,
+ json={
+ "detail": {
+ "message": "The Dag was not found",
+ "reason": "not_found",
+ }
+ },
+ )
+
+ client = make_client(transport=httpx.MockTransport(handle_request))
+
+ with pytest.raises(ServerResponseError) as exc_info:
+ client.dags.get(dag_id=crafted_dag_id)
+
+ assert requests_seen[0].url.raw_path ==
b"/dags/x%2F..%2F..%2Fvariables%2Fsecret_key"
+ assert requests_seen[0].url.path != "/variables/secret_key"
+ assert "super-secret-value" not in str(exc_info.value)
+
def test_get_not_found(self):
"""Test that getting a missing dag raises a server response error."""