fix: handle undefined template variables safely in query rendering. (#35009)

This commit is contained in:
Levis Mbote
2026-01-07 21:44:03 +03:00
committed by GitHub
parent 0c1edd4568
commit dfdf8e75d8
3 changed files with 93 additions and 3 deletions

View File

@@ -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):

View File

@@ -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(

View File

@@ -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)