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):
