You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2026/08/04 16:50:49 UTC

(superset) branch fix-jinja-undefined-error-estimate created (now 98156b4f6e9)

This is an automated email from the ASF dual-hosted git repository.

eschutho pushed a change to branch fix-jinja-undefined-error-estimate
in repository https://gitbox.apache.org/repos/asf/superset.git


      at 98156b4f6e9 fix(sqllab): wrap process_template() in QueryEstimationCommand to prevent raw UndefinedError leak

This branch includes the following new commits:

     new 98156b4f6e9 fix(sqllab): wrap process_template() in QueryEstimationCommand to prevent raw UndefinedError leak

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: fix(sqllab): wrap process_template() in QueryEstimationCommand to prevent raw UndefinedError leak

Posted by el...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

eschutho pushed a commit to branch fix-jinja-undefined-error-estimate
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 98156b4f6e9536ed907ab89870e47d2720bfcbab
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Tue Aug 4 16:40:29 2026 +0000

    fix(sqllab): wrap process_template() in QueryEstimationCommand to prevent raw UndefinedError leak
    
    BaseTemplateProcessor.process_template() has a bare `raise` fallback for
    jinja2 UndefinedError on undefined attribute/subscript access (e.g.
    {{ foo.bar }}) that doesn't match its "is it a function call" heuristic.
    QueryEstimationCommand.run() called process_template() with no try/except
    around it, so this raw jinja2.exceptions.UndefinedError propagated past the
    API layer and hit Flask's catch-all handler, producing an opaque 500 instead
    of a typed 4xx error for a bad Jinja template typed into SQL Lab's cost
    estimator.
    
    Wrap the call in a try/except TemplateError and convert it into a
    SupersetErrorException(status=400), matching the broad-catch pattern
    ExecuteSqlCommand already uses for the same class of leak in the sibling
    execute.py command.
---
 superset/commands/sql_lab/estimate.py              | 13 +++++++-
 tests/unit_tests/commands/sql_lab/test_estimate.py | 38 ++++++++++++++++++++++
 2 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/superset/commands/sql_lab/estimate.py b/superset/commands/sql_lab/estimate.py
index 7d7ef228c2e..fd68e201c63 100644
--- a/superset/commands/sql_lab/estimate.py
+++ b/superset/commands/sql_lab/estimate.py
@@ -21,6 +21,7 @@ from typing import Any, TypedDict
 
 from flask import current_app as app
 from flask_babel import gettext as __
+from jinja2.exceptions import TemplateError
 
 from superset import db, is_feature_enabled, security_manager
 from superset.commands.base import BaseCommand
@@ -150,7 +151,17 @@ class QueryEstimationCommand(BaseCommand):
         sql = self._sql
         if self._template_params:
             template_processor = get_template_processor(self._database)
-            sql = template_processor.process_template(sql, **self._template_params)
+            try:
+                sql = template_processor.process_template(sql, **self._template_params)
+            except TemplateError as ex:
+                raise SupersetErrorException(
+                    SupersetError(
+                        message=str(ex),
+                        error_type=SupersetErrorType.GENERIC_COMMAND_ERROR,
+                        level=ErrorLevel.ERROR,
+                    ),
+                    status=400,
+                ) from ex
 
         # Apply the same SQL security controls used by the execution path
         # (sql_lab.execute_sql_statements) so cost estimation cannot be used to
diff --git a/tests/unit_tests/commands/sql_lab/test_estimate.py b/tests/unit_tests/commands/sql_lab/test_estimate.py
index 61afd2bd97b..c59f2f7bc68 100644
--- a/tests/unit_tests/commands/sql_lab/test_estimate.py
+++ b/tests/unit_tests/commands/sql_lab/test_estimate.py
@@ -377,3 +377,41 @@ def test_apply_sql_security_propagates_engine_schema_gate(
 
     # RLS injection must not happen once the schema gate has rejected the query.
     mock_apply_rls.assert_not_called()
+
+
+# ---------------------------------------------------------------------------
+# process_template() error handling: raw jinja2 errors must not leak from run()
+# ---------------------------------------------------------------------------
+
+
+@patch("superset.commands.sql_lab.estimate.get_template_processor")
+@patch("superset.commands.sql_lab.estimate.security_manager", new_callable=MagicMock)
+@patch("superset.commands.sql_lab.estimate.db")
+def test_run_wraps_raw_jinja_undefined_error(
+    mock_db: MagicMock,
+    mock_security_manager: MagicMock,
+    mock_get_template_processor: MagicMock,
+) -> None:
+    """A raw jinja2 ``UndefinedError`` from ``process_template()`` (the
+    bare-raise fallback in ``BaseTemplateProcessor.process_template`` for
+    undefined attribute/subscript access, e.g. ``{{ foo.bar }}``) must not
+    leak past ``run()`` -- it should surface as a typed
+    ``SupersetErrorException``, matching the ``ExecuteSqlCommand`` sibling's
+    broad-catch pattern in ``commands/sql_lab/execute.py``."""
+    from jinja2.exceptions import UndefinedError
+
+    mock_database = MagicMock()
+    mock_db.session.query.return_value.get.return_value = mock_database
+    mock_security_manager.raise_for_access.return_value = None
+    mock_get_template_processor.return_value.process_template.side_effect = (
+        UndefinedError("'foo' is undefined")
+    )
+
+    command = QueryEstimationCommand(
+        _make_params(sql="SELECT {{ foo.bar }}", template_params={"x": 1})
+    )
+    with pytest.raises(SupersetErrorException) as exc_info:
+        command.run()
+
+    assert exc_info.value.status == 400
+    assert exc_info.value.error.error_type == SupersetErrorType.GENERIC_COMMAND_ERROR