This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-3-test by this push:
new 3db8e458893 [v3-3-test] reject backslash-after-scheme urls in
is_safe_url (#70215) (#70515)
3db8e458893 is described below
commit 3db8e458893a88e61a9305e7a9a48690e8fffeda
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Jul 27 17:01:11 2026 +0200
[v3-3-test] reject backslash-after-scheme urls in is_safe_url (#70215)
(#70515)
(cherry picked from commit eeeb6f02a88791f3c1f9b14c92e3b0b70c5d38a0)
Co-authored-by: Samina <[email protected]>
---
airflow-core/src/airflow/api_fastapi/core_api/security.py | 9 +++++----
airflow-core/tests/unit/api_fastapi/core_api/test_security.py | 8 ++++++++
2 files changed, 13 insertions(+), 4 deletions(-)
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 5f85b68e6b1..720423797b5 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/security.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py
@@ -1007,12 +1007,13 @@ def is_safe_url(target_url: str, request: Request |
None = None) -> bool:
# 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(("//", "/\\", "\\/", "\\\\")):
+ # The same thing also happens with \ where under WHATWG \ are translated
to /, including
+ # after a scheme, so "https:\\host" is an authority for a browser but a
path for urllib.
+ target_url = unquote(target_url).strip().replace("\\", "/")
+ 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
+ parsed_target = urlparse(urljoin(base_url, target_url)) # Resolves
relative URLs
base_path = parsed_base.path or "/"
target_path = parsed_target.path or "/"
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 d7614f82539..8824969f976 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
@@ -632,6 +632,14 @@ class TestFastApiSecurity:
("\\\\some_netlock.com/prefix", False),
# encoded url
("%5C%5C%5C%5Csome_netlock.com/prefix", False),
+ # \ after the scheme, which a browser reads as the start of the
authority
+ ("https:\\\\some_netlock.com", False),
+ ("https:/\\some_netlock.com", False),
+ ("https:\\/some_netlock.com", False),
+ ("https%3A%5C%5Csome_netlock.com", False),
+ # a single leading \ still resolves to a same-origin path
+ ("\\some_page", True),
+ ("/some_page", True),
],
)
def test_is_safe_url_without_prefix(self, url, expected_is_safe):