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/09/04 16:42:54 UTC

(superset) 01/01: fix(sqllab): wrap cost-estimate JSON parse to prevent raw JSONDecodeError leak

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

eschutho pushed a commit to branch fix-presto-estimate-cost-jsondecodeerror
in repository https://gitbox.apache.org/repos/asf/superset.git

commit cd109e09f67f76f4111b3677aee625a86ba1236f
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Fri Sep 4 16:42:31 2026 +0000

    fix(sqllab): wrap cost-estimate JSON parse to prevent raw JSONDecodeError leak
    
    QueryEstimationCommand.run() calls estimate_query_cost(), which for
    Presto/Trino parses the EXPLAIN (TYPE IO, FORMAT JSON) output via
    simplejson-backed superset.utils.json (allow_nan=False). When a table
    lacks computed statistics, the engine emits a literal NaN token, so
    json.loads() raises a raw simplejson JSONDecodeError that propagates
    uncaught through the command and the /api/v1/sqllab/estimate/ endpoint,
    surfacing to the user as an opaque 500 GENERIC_BACKEND_ERROR crash.
    
    Catch json.JSONDecodeError as a sibling of the existing
    SupersetTimeoutException handler and convert it to a typed
    SupersetErrorException (500, GENERIC_BACKEND_ERROR) -- a backend/driver
    response-parsing failure, not user input error -- mirroring the sibling
    TemplateError conversion in the same function (#42757).
    
    Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
---
 superset/commands/sql_lab/estimate.py              | 14 +++++++-
 tests/unit_tests/commands/sql_lab/test_estimate.py | 42 ++++++++++++++++++++++
 2 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/superset/commands/sql_lab/estimate.py b/superset/commands/sql_lab/estimate.py
index c73996f18e3..cf0be98ff0c 100644
--- a/superset/commands/sql_lab/estimate.py
+++ b/superset/commands/sql_lab/estimate.py
@@ -37,7 +37,7 @@ from superset.exceptions import (
 from superset.jinja_context import get_template_processor
 from superset.models.core import Database
 from superset.sql.parse import SQLScript
-from superset.utils import core as utils
+from superset.utils import core as utils, json
 from superset.utils.rls import apply_rls
 
 logger = logging.getLogger(__name__)
@@ -209,6 +209,18 @@ class QueryEstimationCommand(BaseCommand):
                 ),
                 status=500,
             ) from ex
+        except json.JSONDecodeError as ex:
+            logger.exception(ex)
+            raise SupersetErrorException(
+                SupersetError(
+                    message=__(
+                        "Unable to parse the cost estimate returned by the database."
+                    ),
+                    error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
+                    level=ErrorLevel.ERROR,
+                ),
+                status=500,
+            ) from ex
 
         spec = self._database.db_engine_spec
         query_cost_formatters: dict[str, Any] = app.config[
diff --git a/tests/unit_tests/commands/sql_lab/test_estimate.py b/tests/unit_tests/commands/sql_lab/test_estimate.py
index a34123af850..e8a1c62820a 100644
--- a/tests/unit_tests/commands/sql_lab/test_estimate.py
+++ b/tests/unit_tests/commands/sql_lab/test_estimate.py
@@ -445,3 +445,45 @@ def test_run_wraps_raw_jinja_undefined_error(
 
     assert exc_info.value.status == 400
     assert exc_info.value.error.error_type == SupersetErrorType.GENERIC_COMMAND_ERROR
+
+
+# ---------------------------------------------------------------------------
+# estimate_query_cost() error handling: a raw simplejson JSONDecodeError from
+# parsing the engine's cost-estimate response must not leak from run()
+# ---------------------------------------------------------------------------
+
+
+@patch("superset.commands.sql_lab.estimate.security_manager", new_callable=MagicMock)
+@patch("superset.commands.sql_lab.estimate.DatabaseDAO")
+def test_run_wraps_raw_jsondecodeerror_from_cost_estimation(
+    mock_dao: MagicMock,
+    mock_security_manager: MagicMock,
+) -> None:
+    """A raw ``simplejson.JSONDecodeError`` raised while parsing the engine's
+    cost-estimate response (e.g. Presto/Trino ``EXPLAIN (TYPE IO, FORMAT JSON)``
+    emitting a bare ``NaN`` token for tables lacking computed statistics) must
+    not leak past ``run()`` -- it should surface as a typed
+    ``SupersetErrorException`` with a 500 ``GENERIC_BACKEND_ERROR``, mirroring
+    the sibling ``TemplateError`` conversion in the same function."""
+    # ``superset.utils.json`` is simplejson-backed, so ``json.JSONDecodeError``
+    # here *is* ``simplejson.errors.JSONDecodeError`` -- the concrete class the
+    # Presto/Trino cost-estimate parse raises, which is distinct from the stdlib
+    # ``json.JSONDecodeError``. Using the util keeps us off the banned direct
+    # ``simplejson`` import while exercising the exact raised type.
+    from superset.utils import json
+
+    mock_database = MagicMock()
+    mock_dao.find_by_id.return_value = mock_database
+    mock_security_manager.raise_for_access.return_value = None
+    # The raw class actually raised by superset.utils.json (simplejson-backed)
+    # when the driver response contains a literal NaN token.
+    mock_database.db_engine_spec.estimate_query_cost.side_effect = json.JSONDecodeError(
+        "Expecting value", "{}", 0
+    )
+
+    command = QueryEstimationCommand(_make_params())
+    with pytest.raises(SupersetErrorException) as exc_info:
+        command.run()
+
+    assert exc_info.value.status == 500
+    assert exc_info.value.error.error_type == SupersetErrorType.GENERIC_BACKEND_ERROR