Skip to content

fix(sqla): guard unprotected Jinja template rendering in ExploreMixin - #44172

Open
eschutho wants to merge 1 commit into
masterfrom
fix-unprotected-jinja-template-helpers
Open

fix(sqla): guard unprotected Jinja template rendering in ExploreMixin#44172
eschutho wants to merge 1 commit into
masterfrom
fix-unprotected-jinja-template-helpers

Conversation

@eschutho

@eschutho eschutho commented Sep 11, 2026

Copy link
Copy Markdown
Member

SUMMARY

Two process_template call sites in ExploreMixinget_timestamp_expression() and convert_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 raises UndefinedError or TemplateError, the raw jinja2 exception escapes unhandled instead of being surfaced as a QueryObjectValidationError.

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_template wraps parse-time TemplateSyntaxError as SupersetSyntaxErrorException, but during the render phase it only catches RecursionError and a narrow pattern-matched UndefinedError; any other UndefinedError and any other TemplateError subtype raised during rendering escape raw. Both methods are called from get_sqla_query() (the main chart-rendering / SQL Lab query path) with no upstream catch.

Fix: Wrap both process_template calls with the same two-clause handler (except UndefinedErrorQueryObjectValidationError, then except (TemplateError, SupersetSyntaxErrorException)QueryObjectValidationError), mirroring the existing pattern in get_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

  1. Create a dataset with a calculated column expression containing {{ undefined_var.attr }}
  2. Try to query/chart using that column
  3. Before: raw jinja2.exceptions.UndefinedError escapes, producing an opaque 500 error
  4. After: QueryObjectValidationError with message "Calculated column template error: 'undefined_var' is undefined" — a clean, user-facing error

Automated regression tests are included covering both UndefinedError and TemplateSyntaxError for both fixed methods.

ADDITIONAL INFORMATION

  • Has associated issue: sc-120357
  • Required feature flags:
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API
  • Removes existing feature or API

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>
@bito-code-review

bito-code-review Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #09dae3

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 7d4f277..7d4f277
    • superset/models/helpers.py
    • tests/unit_tests/connectors/sqla/models_test.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@netlify

netlify Bot commented Sep 11, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 7d4f277
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6aa42f6cb3116100086285df
😎 Deploy Preview https://deploy-preview-44172--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 11.11111% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.41%. Comparing base (774a511) to head (7d4f277).

Files with missing lines Patch % Lines
superset/models/helpers.py 11.11% 16 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (774a511) and HEAD (7d4f277). Click for more details.

HEAD has 126 uploads less than BASE
Flag BASE (774a511) HEAD (7d4f277)
python 71 5
presto 13 1
hive 13 1
sqlite 13 1
postgres 13 1
mysql 13 1
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     
Flag Coverage Δ
hive 37.33% <0.00%> (-0.01%) ⬇️
mysql 56.85% <11.11%> (-0.01%) ⬇️
postgres 56.88% <11.11%> (-0.02%) ⬇️
presto 39.22% <0.00%> (-0.01%) ⬇️
python 57.16% <11.11%> (-27.39%) ⬇️
sqlite 56.58% <11.11%> (-0.01%) ⬇️
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rebenitez1802 rebenitez1802 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants