This is an automated email from the ASF dual-hosted git repository. kaxilnaik pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit e9d623618a353e19234f109c80c0a0914697db56 Author: Kevin Yang <[email protected]> AuthorDate: Wed Sep 17 11:45:57 2025 -0400 Fix redirection to 'next' url raises an unsafe error (#55704) * precommit fix * add test case for base_url unset * fix the check logic failed for relative url by always unquote (cherry picked from commit 9bc58a20e58a8b8eee9cf2fbe679f40172abc55d) --- .../src/airflow/api_fastapi/core_api/security.py | 4 ++-- .../unit/api_fastapi/core_api/test_security.py | 23 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 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 84bd0ccdd29..50ff119cbbc 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/security.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py @@ -19,7 +19,7 @@ from __future__ import annotations from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Annotated, cast -from urllib.parse import ParseResult, urljoin, urlparse +from urllib.parse import ParseResult, unquote, urljoin, urlparse from fastapi import Depends, HTTPException, Request, status from fastapi.security import HTTPBearer, OAuth2PasswordBearer @@ -484,7 +484,7 @@ def is_safe_url(target_url: str, request: Request | None = None) -> bool: return True for base_url, parsed_base in parsed_bases: - parsed_target = urlparse(urljoin(base_url, target_url)) # Resolves relative URLs + parsed_target = urlparse(urljoin(base_url, unquote(target_url))) # Resolves relative URLs target_path = Path(parsed_target.path).resolve() 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 29efa6eae0e..1be6e8b5667 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 @@ -154,6 +154,12 @@ class TestFastApiSecurity: ("/some_other_page/", False), # traversal, escaping the `prefix` folder ("/../../../../some_page?with_param=3", False), + # encoded url + ("https%3A%2F%2Frequesting_server_base_url.com%2Fprefix2", True), + ("https%3A%2F%2Fserver_base_url.com%2Fprefix", True), + ("https%3A%2F%2Fsome_netlock.com%2Fprefix%2Fsome_page%3Fwith_param%3D3", False), + ("https%3A%2F%2Frequesting_server_base_url.com%2Fprefix2%2Fsub_path", True), + ("%2F..%2F..%2F..%2F..%2Fsome_page%3Fwith_param%3D3", False), ], ) @conf_vars({("api", "base_url"): "https://server_base_url.com/prefix"}) @@ -162,6 +168,23 @@ 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", + [ + ("https://server_base_url.com/prefix", False), + ("https://requesting_server_base_url.com/prefix2", True), + ("prefix/some_other", False), + ("https%3A%2F%2Fserver_base_url.com%2Fprefix", False), + ("https%3A%2F%2Frequesting_server_base_url.com%2Fprefix2", True), + ("https%3A%2F%2Frequesting_server_base_url.com%2Fprefix2%2Fsub_path", True), + ("%2F..%2F..%2F..%2F..%2Fsome_page%3Fwith_param%3D3", False), + ], + ) + def test_is_safe_url_with_base_url_unset(self, url, expected_is_safe): + request = Mock() + request.base_url = "https://requesting_server_base_url.com/prefix2" + assert is_safe_url(url, request=request) == expected_is_safe + @pytest.mark.db_test @pytest.mark.parametrize( "team_name",
