fix(sqla): guard unprotected Jinja template rendering in ExploreMixin - #44172
fix(sqla): guard unprotected Jinja template rendering in ExploreMixin#44172eschutho wants to merge 1 commit into
Conversation
Wrap `process_template` calls in `get_timestamp_expression` and `convert_tbl_column_to_sqla_col` with the same two-clause exception handler that `get_rendered_sql` already uses, so that `UndefinedError` and `TemplateError` surface as `QueryObjectValidationError` instead of escaping as raw jinja2 exceptions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review Agent Run #09dae3Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #44172 +/- ##
===========================================
- Coverage 80.16% 66.41% -13.75%
===========================================
Files 2925 2925
Lines 172563 172579 +16
Branches 40058 40060 +2
===========================================
- Hits 138337 114621 -23716
- Misses 31629 55579 +23950
+ Partials 2597 2379 -218
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
rebenitez1802
left a comment
There was a problem hiding this comment.
Request changes: the calculated-column half is a correct, faithful fix, but the time-column half guards a method with no production caller — the real temporal path still leaks raw UndefinedError, and two new tests assert a guarantee that doesn't hold in production.
🔴 High — Time-column guard is dead code; the live path still leaks
get_timestamp_expression is patched on ExploreMixin (helpers.py:4386), but that method has no production caller. TableColumn doesn't subclass ExploreMixin, and all four call sites (helpers.py:4089/4676/4779/5016) invoke get_timestamp_expression on TableColumn objects with kwargs (apply_dataset_offset=, sql_shifted_temporal_labels=) that only exist on TableColumn.get_timestamp_expression (models.py:1260). That method still wraps process_template with except SupersetSyntaxErrorException only (models.py:1296), so a Jinja error in a time-grain'd temporal calculated column (e.g. {{ nonexistent_var.attr }}) still escapes as raw UndefinedError → 500 — exactly the bug this PR claims to fix. The two get_timestamp_expression tests pass only because they bind the mixin method manually via __get__, giving false green coverage. Fix: move the two-clause handler into TableColumn.get_timestamp_expression at models.py:1295 (where the live catch is), and either delete the now-unused ExploreMixin.get_timestamp_expression or retarget the tests at the real dispatch path. (The convert_tbl_column_to_sqla_col half is genuinely correct — it's ExploreMixin-only and called as self.….)
🟡 Medium — SupersetTemplateException / undefined-function templates bypass the wrapping
The new handlers (and the reference they copy) catch UndefinedError and (TemplateError, SupersetSyntaxErrorException), but process_template also raises UndefinedTemplateFunctionException for {{ missing_macro() }} and SupersetTemplateException for recursion/internal errors (jinja_context.py:1053/1060/1069) — neither is caught. So on the working calculated-column path, {{ some_undefined_func() }} still skips the intended "Calculated column template error" wrapping. Impact is softened because SupersetTemplateException is a SupersetException (rendered as a structured error, not a raw 500), so this is a coverage/consistency gap rather than a crash. Fix: add SupersetTemplateException to the caught tuple (except (TemplateError, SupersetSyntaxErrorException, SupersetTemplateException)) with a str(ex) fallback for the message, ideally in get_rendered_sql too.
🟢 Low — Sibling render paths in the same query still unguarded (asymmetry)
Reachable from the same get_sqla_query, these still catch only SupersetSyntaxErrorException (or nothing): SqlMetric.get_sqla_col (models.py:1433), TableColumn.get_sqla_col (models.py:1220, the adhoc-column reference path), and the fully-unguarded adhoc-SQL renders _process_metric_sql_expression (helpers.py:1937) and _process_validated_sql_expression (helpers.py:1971). Net asymmetry: a calculated column selected directly in groupby is now guarded, but the same column referenced as an adhoc column, or a saved metric like SUM({{ foo.bar }}), still leaks. Out of this PR's stated scope, but worth a follow-up (or expanding here) since it's the same bug class.
🟢 Low — template_error tests inject an exception process_template never emits
test_*_wraps_jinja_template_error mock process_template.side_effect = TemplateSyntaxError(...), but process_template always re-wraps parse-time TemplateSyntaxError as SupersetSyntaxErrorException (jinja_context.py:1034) — a raw TemplateSyntaxError never reaches the handler in production. Because TemplateSyntaxError is a TemplateError, the tests only exercise the ex.message branch; the realistic else: str(ex.errors[0].message …) branch is never covered. Fix: set the side effect to a SupersetSyntaxErrorException (built from a SupersetError) to cover the real branch and the .errors[0] indexing.
🟢 Low — Test assertion/setup nits
The four tests match= only the static prefix ("Time column template error", etc.), not the interpolated original message — so a refactor that blanks msg= to a constant would still pass; consider match=r"…template error.*nonexistent_var". Separately, the two convert_* tests copy database/catalog/schema/engine/_validate_stored_expression setup from …rejects_stored_subquery, but process_template raises before any of it runs — dead setup that misleads readers; drop it (or factor the shared scaffolding into a parametrized helper).
🟢 Low — Pattern nits
The new copies drop the reference's else: # SupersetSyntaxErrorException disambiguator (helpers.py:3578); worth restoring since the else silently corresponds to the second tuple member with a different error shape. The two-clause handler now appears three times in one file — if you dedupe, extract only the _template_error_message(ex) dispatch and keep the _()-wrapped raise inline at each site so pybabel extraction still picks up the strings.
SUMMARY
Two
process_templatecall sites inExploreMixin—get_timestamp_expression()andconvert_tbl_column_to_sqla_col()— lack exception handling for Jinja template errors. When a user writes a Jinja expression (e.g.{{ filter_values('col')[0] }}or{{ undefined_var.attr }}) in a time column or calculated column, and the template raisesUndefinedErrororTemplateError, the raw jinja2 exception escapes unhandled instead of being surfaced as aQueryObjectValidationError.This is the same bug class fixed by #42366 and proposed in #42401 for
get_rendered_sql(), which already has the correct two-clause exception handler. These two sibling methods in the same class were missed.Root cause:
BaseTemplateProcessor.process_templatewraps parse-timeTemplateSyntaxErrorasSupersetSyntaxErrorException, but during the render phase it only catchesRecursionErrorand a narrow pattern-matchedUndefinedError; any otherUndefinedErrorand any otherTemplateErrorsubtype raised during rendering escape raw. Both methods are called fromget_sqla_query()(the main chart-rendering / SQL Lab query path) with no upstream catch.Fix: Wrap both
process_templatecalls with the same two-clause handler (except UndefinedError→QueryObjectValidationError, thenexcept (TemplateError, SupersetSyntaxErrorException)→QueryObjectValidationError), mirroring the existing pattern inget_rendered_sql(). All needed imports were already present in the file.This is additive exception handling only — no behavior change for the success path.
BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
N/A — backend-only change affecting error handling, no UI changes.
TESTING INSTRUCTIONS
{{ undefined_var.attr }}jinja2.exceptions.UndefinedErrorescapes, producing an opaque 500 errorQueryObjectValidationErrorwith message "Calculated column template error: 'undefined_var' is undefined" — a clean, user-facing errorAutomated regression tests are included covering both
UndefinedErrorandTemplateSyntaxErrorfor both fixed methods.ADDITIONAL INFORMATION