This is an automated email from the ASF dual-hosted git repository.
potiuk 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 e7ffeb7c39b reject mixed-separator path traversal in imap attachment
names (#70665)
e7ffeb7c39b is described below
commit e7ffeb7c39b020cceaf2e26df8343980f896d2f4
Author: Samina <[email protected]>
AuthorDate: Sat Aug 1 09:06:33 2026 +0530
reject mixed-separator path traversal in imap attachment names (#70665)
* reject mixed-separator path traversal in imap attachment names
* Use os.altsep for separator-agnostic imap traversal check
Signed-off-by: bibi samina <[email protected]>
---------
Signed-off-by: bibi samina <[email protected]>
---
.../imap/src/airflow/providers/imap/hooks/imap.py | 10 ++++++-
providers/imap/tests/unit/imap/hooks/test_imap.py | 33 ++++++++++++++++++++++
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/providers/imap/src/airflow/providers/imap/hooks/imap.py
b/providers/imap/src/airflow/providers/imap/hooks/imap.py
index 40a4711d906..65d80bd3a0d 100644
--- a/providers/imap/src/airflow/providers/imap/hooks/imap.py
+++ b/providers/imap/src/airflow/providers/imap/hooks/imap.py
@@ -332,7 +332,15 @@ class ImapHook(BaseHook):
return os.path.islink(self._correct_path(name, local_output_directory))
def _is_escaping_current_directory(self, name: str) -> bool:
- return f"..{os.sep}" in name
+ # On Windows os.altsep is "/", so a "../" attachment name traverses
even
+ # though os.sep is a backslash and the old f"..{os.sep}" check missed
it.
+ # Split on every separator the host recognises and reject any ".."
+ # component. Using os.altsep keeps backslashes as ordinary characters
on
+ # POSIX, where they are legal in filenames.
+ normalised = name.replace(os.sep, "/")
+ if os.altsep:
+ normalised = normalised.replace(os.altsep, "/")
+ return any(part == ".." for part in normalised.split("/"))
def _correct_path(self, name: str, local_output_directory: str) -> str:
return (
diff --git a/providers/imap/tests/unit/imap/hooks/test_imap.py
b/providers/imap/tests/unit/imap/hooks/test_imap.py
index bb6d4fc7a68..86fda41522e 100644
--- a/providers/imap/tests/unit/imap/hooks/test_imap.py
+++ b/providers/imap/tests/unit/imap/hooks/test_imap.py
@@ -409,6 +409,39 @@ class TestImapHook:
mock_open_method.assert_not_called()
mock_open_method.return_value.write.assert_not_called()
+ @patch("airflow.providers.imap.hooks.imap.os.altsep", None)
+ @patch("airflow.providers.imap.hooks.imap.os.sep", "/")
+ @pytest.mark.parametrize(
+ ("name", "expected"),
+ [
+ ("../test1.csv", True),
+ ("subdir/../../test1.csv", True),
+ ("..", True),
+ ("test1.csv", False),
+ ("report..final.csv", False),
+ # On POSIX a backslash is an ordinary filename character, not a
separator.
+ ("..\\..\\test1.csv", False),
+ ],
+ )
+ def test_is_escaping_current_directory_on_posix(self, name, expected):
+ assert ImapHook()._is_escaping_current_directory(name) is expected
+
+ @patch("airflow.providers.imap.hooks.imap.os.altsep", "/")
+ @patch("airflow.providers.imap.hooks.imap.os.sep", "\\")
+ @pytest.mark.parametrize(
+ ("name", "expected"),
+ [
+ ("..\\..\\test1.csv", True),
+ ("../../test1.csv", True),
+ ("subdir\\..\\..\\test1.csv", True),
+ ("..", True),
+ ("test1.csv", False),
+ ("report..final.csv", False),
+ ],
+ )
+ def test_is_escaping_current_directory_on_windows(self, name, expected):
+ assert ImapHook()._is_escaping_current_directory(name) is expected
+
@patch("airflow.providers.imap.hooks.imap.os.path.islink",
return_value=True)
@patch(open_string, new_callable=mock_open)
@patch(imaplib_string)