mirror of
https://github.com/apache/superset.git
synced 2026-04-25 02:55:07 +00:00
fix: handle undefined template variables safely in query rendering. (#35009)
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user