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

beto pushed a commit to branch url-redirect
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 073f965cb50b688edd5bf0fe50c35af4380dce7f
Author: Beto Dealmeida <[email protected]>
AuthorDate: Wed Dec 17 14:55:54 2025 -0500

    Fixes
---
 superset/utils/link_redirect.py              | 19 +++++++++++++++----
 superset/views/redirect.py                   |  9 +++++++--
 tests/integration_tests/security_tests.py    |  1 +
 tests/unit_tests/utils/test_link_redirect.py |  1 +
 4 files changed, 24 insertions(+), 6 deletions(-)

diff --git a/superset/utils/link_redirect.py b/superset/utils/link_redirect.py
index f2d078d5e3..f6b6ce7959 100644
--- a/superset/utils/link_redirect.py
+++ b/superset/utils/link_redirect.py
@@ -67,18 +67,21 @@ def _get_redirect_url(original_url: str, base_url: str) -> 
str:
     return f"{base}/redirect/?url={quote(original_url, safe='')}"
 
 
-def _process_link_element(link: Any, base_host: str, base_url: str) -> None:
+def _process_link_element(link: Any, base_host: str, base_url: str) -> bool:
     """Process a single link element for external URL redirection."""
     original_url = link["href"].strip()
 
     if not original_url or "/redirect?" in original_url:
-        return
+        return False
+
+    modified = False
 
     # Handle protocol-relative URLs (e.g., //evil.com/foo)
     if original_url.startswith("//"):
         # Convert to https for processing
         original_url = "https:" + original_url
         link["href"] = original_url
+        modified = True
 
     # Parse the URL
     parsed_url = urlparse(original_url)
@@ -91,6 +94,7 @@ def _process_link_element(link: Any, base_host: str, 
base_url: str) -> None:
         if link_host and link_host.lower() != base_host.lower():
             redirect_url = _get_redirect_url(original_url, base_url)
             link["href"] = redirect_url
+            modified = True
 
             # Optionally add a visual indicator
             if current_app.config.get("ALERT_REPORTS_EXTERNAL_LINK_INDICATOR", 
True):
@@ -98,8 +102,11 @@ def _process_link_element(link: Any, base_host: str, 
base_url: str) -> None:
                 existing_class = link.get("class", [])
                 if isinstance(existing_class, str):
                     existing_class = existing_class.split()
-                existing_class.append("external-link")
+                if "external-link" not in existing_class:
+                    existing_class.append("external-link")
+                    modified = True
                 link["class"] = existing_class
+    return modified
 
 
 def process_html_links(html_content: str, base_url: Optional[str] = None) -> 
str:
@@ -135,8 +142,12 @@ def process_html_links(html_content: str, base_url: 
Optional[str] = None) -> str
             return html_content
 
         # Find all anchor tags with href attribute and process them
+        modified = False
         for link in soup.find_all("a", href=True):
-            _process_link_element(link, base_host, resolved_base_url)
+            modified |= _process_link_element(link, base_host, 
resolved_base_url)
+
+        if not modified:
+            return html_content
 
         # Return the modified HTML
         return str(soup)
diff --git a/superset/views/redirect.py b/superset/views/redirect.py
index e9df40a8d4..42162a9619 100644
--- a/superset/views/redirect.py
+++ b/superset/views/redirect.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import logging
+from typing import Any
 from urllib.parse import unquote, urlparse
 
 from flask import abort, redirect, request
@@ -29,7 +30,7 @@ from superset.views.base import BaseSupersetView
 
 logger = logging.getLogger(__name__)
 
-DANGEROUS_SCHEMES = ("javascript:", "data:", "vbscript:", "file:")
+DANGEROUS_SCHEMES: tuple[str, ...] = ("javascript:", "data:", "vbscript:", 
"file:")
 
 
 def _normalize_url(url: str) -> str:
@@ -71,7 +72,11 @@ def _is_url_in_trusted_cookie(target_url: str) -> bool:
     """Check if the URL is in the trusted URLs cookie."""
     try:
         trusted_urls_cookie = request.cookies.get("superset_trusted_urls", 
"[]")
-        trusted_urls = json.loads(trusted_urls_cookie)
+        trusted_urls_raw: Any = json.loads(trusted_urls_cookie)
+        if not isinstance(trusted_urls_raw, list):
+            return False
+
+        trusted_urls = [url for url in trusted_urls_raw if isinstance(url, 
str)]
         normalized_target = _normalize_url(target_url)
 
         return any(
diff --git a/tests/integration_tests/security_tests.py 
b/tests/integration_tests/security_tests.py
index 96b2cb3e20..6fdc1704b4 100644
--- a/tests/integration_tests/security_tests.py
+++ b/tests/integration_tests/security_tests.py
@@ -1562,6 +1562,7 @@ class TestRolePermission(SupersetTestCase):
             ["SupersetAuthView", "logout"],
             ["SupersetRegisterUserView", "register"],
             ["SupersetRegisterUserView", "activation"],
+            ["RedirectView", "redirect_warning"],
         ]
         unsecured_views = []
         for view_class in appbuilder.baseviews:
diff --git a/tests/unit_tests/utils/test_link_redirect.py 
b/tests/unit_tests/utils/test_link_redirect.py
index 224e90eb39..48636e1cf0 100644
--- a/tests/unit_tests/utils/test_link_redirect.py
+++ b/tests/unit_tests/utils/test_link_redirect.py
@@ -293,6 +293,7 @@ def test_is_safe_redirect_url_no_base_configured(app):
 def test_process_html_links_invalid_base_url(app):
     """Test behavior with invalid base URL"""
     app.config["WEBDRIVER_BASEURL"] = "not-a-valid-url"
+    app.config["WEBDRIVER_BASEURL_USER_FRIENDLY"] = "not-a-valid-url"
 
     with app.app_context():
         html = '<a href="https://external.com";>External</a>'

Reply via email to