You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/09/16 23:51:02 UTC
(superset) branch master updated: fix(themes): render brand spinner SVG via
data URI (#43504)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 181e9ad3e2b fix(themes): render brand spinner SVG via <img> data URI (#43504)
181e9ad3e2b is described below
commit 181e9ad3e2b0497fa3369a9a7c853633e948934a
Author: n4uu <n4...@pm.me>
AuthorDate: Thu Sep 17 01:50:56 2026 +0200
fix(themes): render brand spinner SVG via <img> data URI (#43504)
Signed-off-by: n4uu <n4...@users.noreply.github.com>
Co-authored-by: n4uu <n4...@users.noreply.github.com>
Co-authored-by: rusackas <ev...@rusackas.com>
Co-authored-by: Claude Sonnet 5 <no...@anthropic.com>
---
superset/templates/superset/spa.html | 13 +++--
superset/views/base.py | 21 +++++++-
tests/unit_tests/views/test_base_theme_helpers.py | 62 +++++++++++++++++++++++
3 files changed, 90 insertions(+), 6 deletions(-)
diff --git a/superset/templates/superset/spa.html b/superset/templates/superset/spa.html
index f0ba0eec534..416e19c6ae7 100644
--- a/superset/templates/superset/spa.html
+++ b/superset/templates/superset/spa.html
@@ -144,11 +144,14 @@
{% set tokens = theme_tokens | default({}) %}
{% set spinner_style = "width: 70px; height: auto; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);" %}
- {% if spinner_svg %}
- <!-- Inline SVG: Either custom theme SVG or default spinner -->
- <div style="{{ spinner_style }}">
- {{ spinner_svg | safe }}
- </div>
+ {% if spinner_svg_data_uri %}
+ <!-- data URI: SVG renders as a passive image, no script execution -->
+ <img
+ src="{{ spinner_svg_data_uri }}"
+ alt=""
+ aria-label="Loading"
+ style="{{ spinner_style }}"
+ />
{% elif tokens.get('brandSpinnerUrl') %}
<!-- Custom URL from theme -->
<img
diff --git a/superset/views/base.py b/superset/views/base.py
index 213e211f952..f5f48fea5f0 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -16,6 +16,7 @@
# under the License.
from __future__ import annotations
+import base64
import copy
import functools
import logging
@@ -754,6 +755,20 @@ def _ensure_static_assets_prefix(url_or_path: str) -> str:
return f"{normalized_prefix}{url_or_path}"
+def _svg_to_data_uri(svg: str | None) -> str | None:
+ """Encode SVG markup as a base64 data URI, or ``None`` if no SVG is given.
+
+ A malformed theme (hand-edited config or a stale database row) could
+ supply a truthy non-string value here; treat that as absent rather than
+ raising, since raising would 500 every SPA render.
+ """
+ if not isinstance(svg, str) or not svg:
+ return None
+ return "data:image/svg+xml;base64," + base64.b64encode(svg.encode("utf-8")).decode(
+ "ascii"
+ )
+
+
def get_spa_template_context(
entry: str | None = "spa",
extra_bootstrap_data: dict[str, Any] | None = None,
@@ -823,6 +838,10 @@ def get_spa_template_context(
# No custom URL either, use default SVG
spinner_svg = get_default_spinner_svg()
+ # Serve the spinner as an <img> data URI (see spa.html). SVG loaded via <img>
+ # can't execute scripts, so rendering doesn't depend on sanitize_svg_content.
+ spinner_svg_data_uri = _svg_to_data_uri(spinner_svg)
+
# Determine default title using the (potentially updated) brandAppName
default_title = theme_tokens.get("brandAppName", "Superset")
@@ -837,7 +856,7 @@ def get_spa_template_context(
),
"theme_tokens": theme_tokens,
"dark_theme_bg": dark_theme_bg,
- "spinner_svg": spinner_svg,
+ "spinner_svg_data_uri": spinner_svg_data_uri,
"default_title": default_title,
**get_language_pack_template_context(payload.get("common") or {}),
**template_kwargs,
diff --git a/tests/unit_tests/views/test_base_theme_helpers.py b/tests/unit_tests/views/test_base_theme_helpers.py
index adc601dee14..1b0e9c5eff0 100644
--- a/tests/unit_tests/views/test_base_theme_helpers.py
+++ b/tests/unit_tests/views/test_base_theme_helpers.py
@@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
+import base64
from unittest.mock import MagicMock, mock_open, patch
from superset.themes.types import ThemeMode
@@ -23,6 +24,7 @@ from superset.views.base import (
_load_theme_from_model,
_merge_theme_dicts,
_process_theme,
+ _svg_to_data_uri,
get_theme_bootstrap_data,
)
@@ -1119,6 +1121,66 @@ class TestGetDefaultSpinnerSvg:
assert "morphPath" in result
+class TestSvgToDataUri:
+ """Test _svg_to_data_uri (see #43504: <img> data URI spinner rendering)"""
+
+ def test_string_input_encoded_as_data_uri(self):
+ """A valid SVG string is base64-encoded into a data URI"""
+ svg = "<svg>ok</svg>"
+ result = _svg_to_data_uri(svg)
+ assert result == (
+ "data:image/svg+xml;base64,"
+ + base64.b64encode(svg.encode("utf-8")).decode("ascii")
+ )
+
+ def test_none_input_returns_none(self):
+ assert _svg_to_data_uri(None) is None
+
+ def test_empty_string_returns_none(self):
+ assert _svg_to_data_uri("") is None
+
+ def test_non_string_input_returns_none(self):
+ """A malformed theme (hand-edited config or a stale database row)
+ could supply a truthy non-string value; it must be treated as
+ absent rather than raising and 500ing SPA rendering."""
+ assert _svg_to_data_uri(True) is None # noqa: FBT003
+ assert _svg_to_data_uri(123) is None
+ assert _svg_to_data_uri({"not": "a string"}) is None
+
+
+class TestSpinnerSvgDataUri:
+ """Regression test for #43503: brandSpinnerSvg must reach spa.html only
+ as a base64 data URI, never as raw markup that could be inlined."""
+
+ @patch("superset.views.base.get_spa_payload")
+ @patch("superset.views.base.app")
+ def test_custom_svg_rendered_only_as_data_uri(self, mock_app, mock_payload):
+ from superset.views.base import get_spa_template_context
+
+ mock_app.config = {}
+ malicious_svg = '<svg onload="alert(1)"><script>alert(1)</script></svg>'
+ mock_payload.return_value = {
+ "common": {
+ "theme": {
+ "default": {
+ "token": {"brandSpinnerSvg": malicious_svg},
+ }
+ }
+ }
+ }
+
+ result = get_spa_template_context("app")
+
+ # The template no longer receives a raw "spinner_svg" it could
+ # render with `| safe`; only the encoded data URI is passed.
+ assert "spinner_svg" not in result
+ assert result["spinner_svg_data_uri"].startswith("data:image/svg+xml;base64,")
+ decoded = base64.b64decode(
+ result["spinner_svg_data_uri"].split(",", 1)[1]
+ ).decode("utf-8")
+ assert decoded == malicious_svg
+
+
class TestThemeCacheInvalidation:
"""Test theme cache invalidation event listeners"""