This is an automated email from the ASF dual-hosted git repository.

rahulvats pushed a commit to branch py-client-sync
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 153239dd3c1e673c61a65f6ca7eb88c794b7a662
Author: Yoann <[email protected]>
AuthorDate: Mon Mar 23 23:14:21 2026 -0700

    fix: block path traversal via ".." in dag_id and run_id (#63296)
    
    * fix: block path traversal via ".." in dag_id and run_id
    
    validate_key() and validate_run_id() allow ".." which can be used
    for path traversal when these values end up in log file paths.
    Reject any key or run_id containing ".." early in validation.
    
    Closes: #63295
    
    * Add newsfragment for breaking change
    
    * fix: add config flag allow_dotdot_in_ids and fix path traversal check in 
create_dagrun
    
    - Add [core] allow_dotdot_in_ids config flag (default: False) so existing
      users with '..' in their IDs can opt out of the blocking behavior
    - Add '..' check in SerializedDAG.create_dagrun() before the regex check,
      fixing the test_dag_run_id_rejects_path_traversal failure where
      create_dagrun raised a regex mismatch error before the traversal check
    - Collapse multi-line raise ValueError in dagrun.py to fix static check
    - Make all '..' checks conditional on allow_dotdot_in_ids config
    
    * refactor: rename allow_dotdot_in_ids to allow_double_dot_in_ids, bump 
version_added to 3.3.0
---
 airflow-core/newsfragments/63296.significant.rst       | 16 ++++++++++++++++
 airflow-core/src/airflow/config_templates/config.yml   | 10 ++++++++++
 airflow-core/src/airflow/models/dagrun.py              |  2 ++
 .../src/airflow/serialization/definitions/dag.py       |  3 +++
 airflow-core/src/airflow/utils/helpers.py              |  4 ++++
 airflow-core/tests/unit/models/test_dagrun.py          | 18 ++++++++++++++++++
 airflow-core/tests/unit/utils/test_helpers.py          | 10 ++++++++++
 7 files changed, 63 insertions(+)

diff --git a/airflow-core/newsfragments/63296.significant.rst 
b/airflow-core/newsfragments/63296.significant.rst
new file mode 100644
index 00000000000..566e21e0de6
--- /dev/null
+++ b/airflow-core/newsfragments/63296.significant.rst
@@ -0,0 +1,16 @@
+Block path traversal via ``..`` in ``dag_id`` and ``run_id``
+
+DAG IDs and run IDs containing ``..`` are now rejected by default to prevent 
path traversal.
+A configuration flag ``[core] allow_double_dot_in_ids`` (default: ``False``) 
is available for
+environments that rely on ``..`` in identifiers.
+
+* Types of change
+
+  * [ ] Dag changes
+  * [x] Config changes
+  * [x] API changes
+  * [ ] CLI changes
+  * [x] Behaviour changes
+  * [ ] Plugin changes
+  * [ ] Dependency changes
+  * [ ] Code interface changes
diff --git a/airflow-core/src/airflow/config_templates/config.yml 
b/airflow-core/src/airflow/config_templates/config.yml
index 0c002f5276c..55313198d5b 100644
--- a/airflow-core/src/airflow/config_templates/config.yml
+++ b/airflow-core/src/airflow/config_templates/config.yml
@@ -439,6 +439,16 @@ core:
       example: ~
       default: "1024"
 
+    allow_double_dot_in_ids:
+      description: |
+        Allow ``..`` (consecutive dots) in DAG IDs and run IDs. By default, 
``..`` is blocked to prevent
+        path traversal attacks. Set to ``True`` only if you have existing DAGs 
or runs whose IDs contain
+        ``..`` and cannot be renamed.
+      version_added: 3.3.0
+      type: boolean
+      example: ~
+      default: "False"
+
     daemon_umask:
       description: |
         The default umask to use for process when run in daemon mode 
(scheduler, worker,  etc.)
diff --git a/airflow-core/src/airflow/models/dagrun.py 
b/airflow-core/src/airflow/models/dagrun.py
index 20ec118f33e..bbff43aad9a 100644
--- a/airflow-core/src/airflow/models/dagrun.py
+++ b/airflow-core/src/airflow/models/dagrun.py
@@ -395,6 +395,8 @@ class DagRun(Base, LoggingMixin):
     def validate_run_id(self, key: str, run_id: str) -> str | None:
         if not run_id:
             return None
+        if ".." in run_id and not airflow_conf.getboolean("core", 
"allow_double_dot_in_ids", fallback=False):
+            raise ValueError(f"The run_id '{run_id}' must not contain '..' to 
prevent path traversal")
         if re.match(RUN_ID_REGEX, run_id):
             return run_id
         regex = airflow_conf.get("scheduler", "allowed_run_id_pattern").strip()
diff --git a/airflow-core/src/airflow/serialization/definitions/dag.py 
b/airflow-core/src/airflow/serialization/definitions/dag.py
index a8fa92d12ce..1ef9ec27625 100644
--- a/airflow-core/src/airflow/serialization/definitions/dag.py
+++ b/airflow-core/src/airflow/serialization/definitions/dag.py
@@ -548,6 +548,9 @@ class SerializedDAG:
         if not isinstance(run_id, str):
             raise ValueError(f"`run_id` should be a str, not {type(run_id)}")
 
+        if ".." in run_id and not airflow_conf.getboolean("core", 
"allow_double_dot_in_ids", fallback=False):
+            raise ValueError(f"The run_id '{run_id}' must not contain '..' to 
prevent path traversal")
+
         # This is also done on the DagRun model class, but SQLAlchemy column
         # validator does not work well for some reason.
         if not re.match(RUN_ID_REGEX, run_id):
diff --git a/airflow-core/src/airflow/utils/helpers.py 
b/airflow-core/src/airflow/utils/helpers.py
index 5e2dd1b9ded..73265463edc 100644
--- a/airflow-core/src/airflow/utils/helpers.py
+++ b/airflow-core/src/airflow/utils/helpers.py
@@ -61,6 +61,10 @@ def validate_key(k: str, max_length: int = 250):
             f"The key {k!r} has to be made of alphanumeric characters, dashes, 
"
             f"dots and underscores exclusively"
         )
+    if ".." in k and not conf.getboolean("core", "allow_double_dot_in_ids", 
fallback=False):
+        raise AirflowException(
+            f"The key {k!r} must not contain consecutive dots ('..') to 
prevent path traversal"
+        )
 
 
 def ask_yesno(question: str, default: bool | None = None, output_fn=print) -> 
bool:
diff --git a/airflow-core/tests/unit/models/test_dagrun.py 
b/airflow-core/tests/unit/models/test_dagrun.py
index b446db9f2b9..dd34c2d10e7 100644
--- a/airflow-core/tests/unit/models/test_dagrun.py
+++ b/airflow-core/tests/unit/models/test_dagrun.py
@@ -3175,6 +3175,24 @@ def test_dag_run_id_config(session, dag_maker, pattern, 
run_id, result):
                 dag_maker.create_dagrun(run_id=run_id, run_type=run_type)
 
 
[email protected]_test
[email protected]_serialized_dag(False)
[email protected](
+    "run_id",
+    [
+        "manual__..%2F..%2Fetc%2Fpasswd",
+        "my..run",
+        "..",
+    ],
+)
+def test_dag_run_id_rejects_path_traversal(session, dag_maker, run_id):
+    """run_id containing '..' should be rejected to prevent path traversal."""
+    with dag_maker():
+        pass
+    with pytest.raises(ValueError, match=r"must not contain '\.\.'"):
+        dag_maker.create_dagrun(run_id=run_id, run_type=DagRunType.MANUAL)
+
+
 def _get_states(dr):
     """
     For a given dag run, get a dict of states.
diff --git a/airflow-core/tests/unit/utils/test_helpers.py 
b/airflow-core/tests/unit/utils/test_helpers.py
index 8e16d118698..ab82f820475 100644
--- a/airflow-core/tests/unit/utils/test_helpers.py
+++ b/airflow-core/tests/unit/utils/test_helpers.py
@@ -140,6 +140,16 @@ class TestHelpers:
                 AirflowException,
             ),
             (" " * 251, f"The key: {' ' * 251} has to be less than 250 
characters", AirflowException),
+            (
+                "my..key",
+                "The key 'my..key' must not contain consecutive dots ('..') to 
prevent path traversal",
+                AirflowException,
+            ),
+            (
+                "..",
+                "The key '..' must not contain consecutive dots ('..') to 
prevent path traversal",
+                AirflowException,
+            ),
         ],
     )
     def test_validate_key(self, key_id, message, exception):

Reply via email to