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 323248e1d7410386d686f2cdd381e37b68ba0e13 Author: Beto Dealmeida <robe...@dealmeida.net> AuthorDate: Thu Sep 4 11:39:41 2025 -0400 More improvements --- docs/docs/configuration/alerts-reports.mdx | 37 ++++++++++++++++++++++ superset/config.py | 4 +++ superset/views/redirect.py | 5 +++ .../integration_tests/views/test_redirect_view.py | 13 ++++++++ tests/unit_tests/utils/test_link_redirect.py | 26 +++++++++++++++ 5 files changed, 85 insertions(+) diff --git a/docs/docs/configuration/alerts-reports.mdx b/docs/docs/configuration/alerts-reports.mdx index 5f4f028957..97847b1c87 100644 --- a/docs/docs/configuration/alerts-reports.mdx +++ b/docs/docs/configuration/alerts-reports.mdx @@ -217,6 +217,43 @@ def alert_dynamic_minimal_interval(**kwargs) -> int: ALERT_MINIMUM_INTERVAL = alert_dynamic_minimal_interval ``` +## Security Configuration + +### External Link Redirection + +For security purposes, Superset automatically processes HTML content in alert and report emails to redirect external links through a warning page. This helps protect users from potentially malicious links. + +#### Configuration Options + +```python +# Enable/disable external link redirection (default: True) +ALERT_REPORTS_ENABLE_LINK_REDIRECT = True + +# Show visual indicators for external links in emails (default: True) +ALERT_REPORTS_EXTERNAL_LINK_INDICATOR = True +``` + +#### How it Works + +1. **HTML Processing**: When generating alert/report emails, Superset scans HTML content for external links +2. **Link Replacement**: External links (those not pointing to your Superset instance) are replaced with redirect URLs +3. **Warning Page**: Users who click external links see a warning page before being redirected +4. **Internal Links**: Links pointing to your Superset instance are not modified + +#### Security Features + +- **Dangerous Scheme Blocking**: Automatically blocks `javascript:`, `data:`, `vbscript:`, and `file:` URLs +- **Host Validation**: Uses your `WEBDRIVER_BASEURL_USER_FRIENDLY` or `WEBDRIVER_BASEURL` configuration to determine internal vs external links +- **Logging**: All redirect attempts are logged for security monitoring + +#### Disabling the Feature + +To disable external link redirection entirely: + +```python +ALERT_REPORTS_ENABLE_LINK_REDIRECT = False +``` + ## Troubleshooting There are many reasons that reports might not be working. Try these steps to check for specific issues. diff --git a/superset/config.py b/superset/config.py index b41992fe1e..613479ce12 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1692,6 +1692,10 @@ ALERT_REPORTS_QUERY_EXECUTION_MAX_TRIES = 1 # Custom width for screenshots ALERT_REPORTS_MIN_CUSTOM_SCREENSHOT_WIDTH = 600 ALERT_REPORTS_MAX_CUSTOM_SCREENSHOT_WIDTH = 2400 +# External link redirection in alert/report emails +ALERT_REPORTS_ENABLE_LINK_REDIRECT = True +# Show visual indicators for external links in emails +ALERT_REPORTS_EXTERNAL_LINK_INDICATOR = True # Set a minimum interval threshold between executions (for each Alert/Report) # Value should be an integer i.e. int(timedelta(minutes=5).total_seconds()) # You can also assign a function to the config that returns the expected integer diff --git a/superset/views/redirect.py b/superset/views/redirect.py index 35e63141f9..4404395aa6 100644 --- a/superset/views/redirect.py +++ b/superset/views/redirect.py @@ -22,6 +22,7 @@ from flask import abort, redirect, request from flask_appbuilder import expose from flask_appbuilder.security.decorators import has_access +from superset import is_feature_enabled from superset.superset_typing import FlaskResponse from superset.utils.link_redirect import is_safe_redirect_url from superset.views.base import SupersetModelView @@ -43,6 +44,10 @@ class RedirectView(SupersetModelView): """ Show a warning page before redirecting to an external URL """ + # Check if ALERT_REPORTS feature is enabled + if not is_feature_enabled("ALERT_REPORTS"): + abort(404, description="Feature not enabled") + # Get the target URL from query parameters target_url = request.args.get("url", "") diff --git a/tests/integration_tests/views/test_redirect_view.py b/tests/integration_tests/views/test_redirect_view.py index b21ca8d559..73b49411c5 100644 --- a/tests/integration_tests/views/test_redirect_view.py +++ b/tests/integration_tests/views/test_redirect_view.py @@ -267,3 +267,16 @@ class TestRedirectView(SupersetTestCase): # Should return 400 for dangerous schemes regardless of case assert response.status_code == 400 + + @with_config({"ALERT_REPORTS": False}) + def test_redirect_feature_flag_disabled(self): + """Test that redirect endpoint returns 404 when ALERT_REPORTS is disabled""" + self.login(username="admin") + + external_url = "https://external.com" + encoded_url = quote(external_url, safe="") + + response = self.client.get(f"/redirect/?url={encoded_url}") + + # Should return 404 when feature is disabled + assert response.status_code == 404 diff --git a/tests/unit_tests/utils/test_link_redirect.py b/tests/unit_tests/utils/test_link_redirect.py index 009d70baca..909fd95625 100644 --- a/tests/unit_tests/utils/test_link_redirect.py +++ b/tests/unit_tests/utils/test_link_redirect.py @@ -300,3 +300,29 @@ def test_process_html_links_invalid_base_url(app): # Should return unchanged HTML when base URL is invalid assert result == html + + +def test_process_html_links_feature_disabled(app): + """Test behavior when feature is disabled""" + app.config["ALERT_REPORTS_ENABLE_LINK_REDIRECT"] = False + + with app.app_context(): + html = '<a href="https://external.com">External</a>' + result = process_html_links(html) + + # Should return unchanged HTML when feature is disabled + assert result == html + + +def test_process_html_links_large_html(app): + """Test processing very large HTML content""" + with app.app_context(): + # Create large HTML content + large_html = ( + "<div>" + '<a href="https://external.com">External</a>' * 1000 + "</div>" + ) + result = process_html_links(large_html) + + # Should still process correctly + assert "/redirect?" in result + assert result.count("/redirect?") == 1000 # All links should be processed