This is an automated email from the ASF dual-hosted git repository.
diegopucci 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 dfdf8e75d8 fix: handle undefined template variables safely in query
rendering. (#35009)
dfdf8e75d8 is described below
commit dfdf8e75d8d702d2db0c4fba8c5f6401e747d078
Author: Levis Mbote <[email protected]>
AuthorDate: Wed Jan 7 21:44:03 2026 +0300
fix: handle undefined template variables safely in query rendering. (#35009)
---
superset/jinja_context.py | 19 ++++++++++--
superset/sqllab/query_render.py | 23 ++++++++++++++-
tests/unit_tests/jinja_context_test.py | 54 ++++++++++++++++++++++++++++++++++
3 files changed, 93 insertions(+), 3 deletions(-)
diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index 673e6d3815..dc9411030e 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -28,8 +28,8 @@ from typing import Any, Callable, cast, TYPE_CHECKING,
TypedDict, Union
import dateutil
from flask import current_app, g, has_request_context, request
from flask_babel import gettext as _
-from jinja2 import DebugUndefined, Environment, TemplateSyntaxError
-from jinja2.exceptions import SecurityError, UndefinedError
+from jinja2 import DebugUndefined, Environment, TemplateSyntaxError,
UndefinedError
+from jinja2.exceptions import SecurityError
from jinja2.sandbox import SandboxedEnvironment
from sqlalchemy.engine.interfaces import Dialect
from sqlalchemy.sql.expression import bindparam
@@ -65,6 +65,13 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
+
+class UndefinedTemplateFunctionException(SupersetTemplateException):
+ """Raised when an undefined function-like Jinja identifier is
encountered."""
+
+ pass
+
+
NONE_TYPE = type(None).__name__
ALLOWED_TYPES = (
NONE_TYPE,
@@ -768,6 +775,14 @@ class BaseTemplateProcessor:
raise SupersetTemplateException(
"Infinite recursion detected in template"
) from ex
+ except UndefinedError as ex:
+ match = re.search(r'["\']([^"\']+)["\']\s+is undefined', str(ex))
+ undefined_name = match.group(1) if match else None
+ if undefined_name and re.search(
+ r"\{\{\s*(?:[\w\.]*\.)?" + re.escape(undefined_name) +
r"\s*\(", sql
+ ):
+ raise UndefinedTemplateFunctionException(str(ex)) from ex
+ raise
class JinjaTemplateProcessor(BaseTemplateProcessor):
diff --git a/superset/sqllab/query_render.py b/superset/sqllab/query_render.py
index 7d41d7fb03..d8effce041 100644
--- a/superset/sqllab/query_render.py
+++ b/superset/sqllab/query_render.py
@@ -66,6 +66,23 @@ class SqlQueryRenderImpl(SqlQueryRender):
except TemplateError as ex:
self._raise_template_exception(ex, execution_context)
return "NOT_REACHABLE_CODE"
+ except Exception as ex:
+ from superset.jinja_context import
UndefinedTemplateFunctionException
+
+ if isinstance(ex, UndefinedTemplateFunctionException):
+ return query_model.sql.strip().strip(";")
+ raise
+
+ def _strip_sql_comments(
+ self,
+ execution_context: SqlJsonExecutionContext,
+ sql: str,
+ ) -> str:
+ from superset.sql.parse import SQLScript
+
+ engine = execution_context.query.database.db_engine_spec.engine
+ script = SQLScript(sql, engine)
+ return script.format(comments=False)
def _validate(
self,
@@ -74,7 +91,11 @@ class SqlQueryRenderImpl(SqlQueryRender):
sql_template_processor: BaseTemplateProcessor,
) -> None:
if is_feature_enabled("ENABLE_TEMPLATE_PROCESSING"):
- syntax_tree = sql_template_processor.env.parse(rendered_query)
+ sql_for_validation = self._strip_sql_comments(
+ execution_context,
+ rendered_query,
+ )
+ syntax_tree = sql_template_processor.env.parse(sql_for_validation)
undefined_parameters = find_undeclared_variables(syntax_tree)
if undefined_parameters:
self._raise_undefined_parameter_exception(
diff --git a/tests/unit_tests/jinja_context_test.py
b/tests/unit_tests/jinja_context_test.py
index 985dd909ee..929c470b31 100644
--- a/tests/unit_tests/jinja_context_test.py
+++ b/tests/unit_tests/jinja_context_test.py
@@ -1639,3 +1639,57 @@ def test_jinja2_server_error_handling(mocker:
MockerFixture) -> None:
assert "Internal Jinja2 template error" in str(exception)
assert "MemoryError" in str(exception)
assert "Out of memory" in str(exception)
+
+
+def test_undefined_template_function_exception(mocker: MockerFixture) -> None:
+ """Test UndefinedTemplateFunctionException for undefined function
identifiers."""
+ from superset.jinja_context import (
+ BaseTemplateProcessor,
+ UndefinedTemplateFunctionException,
+ )
+
+ database = mocker.MagicMock()
+ database.db_engine_spec = mocker.MagicMock()
+
+ processor = BaseTemplateProcessor(database=database)
+
+ template = "SELECT {{ undefined_function() }}"
+ with pytest.raises(UndefinedTemplateFunctionException) as exc_info:
+ processor.process_template(template)
+
+ exception = exc_info.value
+ assert isinstance(exception, UndefinedTemplateFunctionException)
+ assert "undefined" in str(exception).lower()
+
+
+def test_undefined_template_function_exception_with_namespace(
+ mocker: MockerFixture,
+) -> None:
+ """Test namespaced undefined functions raise UndefinedError (not
converted)."""
+ from jinja2.exceptions import UndefinedError
+
+ from superset.jinja_context import BaseTemplateProcessor
+
+ database = mocker.MagicMock()
+ database.db_engine_spec = mocker.MagicMock()
+
+ processor = BaseTemplateProcessor(database=database)
+ template = "SELECT {{ namespace.undefined_function() }}"
+ with pytest.raises(UndefinedError):
+ processor.process_template(template)
+
+
+def test_undefined_template_variable_not_function(mocker: MockerFixture) ->
None:
+ """Test undefined variables with method calls raise UndefinedError."""
+ from jinja2.exceptions import UndefinedError
+
+ from superset.jinja_context import BaseTemplateProcessor
+
+ database = mocker.MagicMock()
+ database.db_engine_spec = mocker.MagicMock()
+
+ processor = BaseTemplateProcessor(database=database)
+
+ template = "SELECT {{ undefined_variable.some_method() }}"
+ with pytest.raises(UndefinedError):
+ processor.process_template(template)