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

pierrejeambrun 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 555f35576e3 Updates the is_url_safe method to reject urls with /// 
(#65557)
555f35576e3 is described below

commit 555f35576e3143dec65a7fb9acd95891841b45f2
Author: Aritra Basu <[email protected]>
AuthorDate: Thu Apr 23 22:55:45 2026 +0530

    Updates the is_url_safe method to reject urls with /// (#65557)
    
    * Updates the is_url_safe method to reject urls with ///
    
    JS's URL constructor sticks to WHATWG's Url parsing for
    special schemes ref: https://url.spec.whatwg.org/#url-parsing
    
    Due to which a host like ///example.com is a valid url in JS
    and resolves to http(s)://example.com if the base url is a http(s)
    URL.
    
    The same thing happens with \.
    
    * Adds unit tests for is_safe_url
---
 airflow-core/src/airflow/api_fastapi/core_api/security.py |  6 ++++++
 .../tests/unit/api_fastapi/core_api/test_security.py      | 15 +++++++++++++++
 2 files changed, 21 insertions(+)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/security.py 
b/airflow-core/src/airflow/api_fastapi/core_api/security.py
index 774cfa9ed6a..941a4c418f5 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/security.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py
@@ -779,6 +779,12 @@ def is_safe_url(target_url: str, request: Request | None = 
None) -> bool:
         # Can't enforce any security check.
         return True
 
+    # According to WHATWG for http/https /// is interpreted as // whereas 
urllib doesnt
+    # this leads to an inconsistency where python returns a target url with 
/// as a valid url
+    # The same thing also happens with \ where under WHATWG \ are translated 
to /
+    target_url = unquote(target_url).strip()
+    if target_url.startswith(("//", "/\\", "\\/", "\\\\")):
+        return False
     for base_url, parsed_base in parsed_bases:
         parsed_target = urlparse(urljoin(base_url, unquote(target_url)))  # 
Resolves relative URLs
 
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/test_security.py 
b/airflow-core/tests/unit/api_fastapi/core_api/test_security.py
index e5cb5f17cf9..9b8a3abd762 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/test_security.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/test_security.py
@@ -413,6 +413,21 @@ class TestFastApiSecurity:
         request.base_url = "https://requesting_server_base_url.com/prefix2";
         assert is_safe_url(url, request=request) == expected_is_safe
 
+    @pytest.mark.parametrize(
+        ("url", "expected_is_safe"),
+        [
+            # Using \ or /// to escape host check
+            ("///some_netlock.com/prefix", False),
+            ("\\\\some_netlock.com/prefix", False),
+            # encoded url
+            ("%5C%5C%5C%5Csome_netlock.com/prefix", False),
+        ],
+    )
+    def test_is_safe_url_without_prefix(self, url, expected_is_safe):
+        request = Mock()
+        request.base_url = "https://requesting_server_base_url.com/";
+        assert is_safe_url(url, request=request) == expected_is_safe
+
     @pytest.mark.parametrize(
         ("url", "expected_is_safe"),
         [

Reply via email to