You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ju...@apache.org on 2026/08/07 18:07:07 UTC
(superset) 04/04: fix(mcp): generic auth errors, required token expiry, and safer auth logging (#40646)
This is an automated email from the ASF dual-hosted git repository.
justinpark pushed a commit to branch 6.2
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 210da7f0ae6d431131467ee9bd53359b5cd57cf4
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Tue Jun 2 08:31:05 2026 -0700
fix(mcp): generic auth errors, required token expiry, and safer auth logging (#40646)
Co-authored-by: Claude Code <no...@anthropic.com>
(cherry picked from commit ca8855dc0370a0c7264859b2b093cb7bbfd39e20)
---
superset/mcp_service/utils/error_sanitization.py | 44 +++++++++++++-
.../mcp_service/utils/test_error_sanitization.py | 70 ++++++++++++++++++++++
2 files changed, 112 insertions(+), 2 deletions(-)
diff --git a/superset/mcp_service/utils/error_sanitization.py b/superset/mcp_service/utils/error_sanitization.py
index 332a3aa7111..8923713a53b 100644
--- a/superset/mcp_service/utils/error_sanitization.py
+++ b/superset/mcp_service/utils/error_sanitization.py
@@ -23,7 +23,29 @@ disclosure (e.g., SQL fragments, schema names, table names) while preserving
actionable error messages for LLM callers.
"""
+import logging
import re
+from typing import Any
+
+logger = logging.getLogger(__name__)
+
+
+def sanitize_for_log(value: Any) -> str:
+ """Escape control characters in attacker-controlled values before logging.
+
+ Claim values (alg, iss, aud, client_id, ...) and error strings are
+ attacker-controlled and may contain newlines or other control characters
+ that could be used to forge or split log lines. Escaping ``\\n``/``\\r``/``\\t``
+ keeps each logged value confined to a single, unambiguous log entry.
+ Backslash is escaped first to avoid double-escaping the replacements.
+ """
+ return (
+ str(value)
+ .replace("\\", "\\\\")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t")
+ )
def _redact_sql_select(error_str: str, error_str_upper: str) -> str:
@@ -63,8 +85,26 @@ def _get_generic_error_message(error_str: str) -> str | None:
return None
-def _sanitize_validation_error(error: Exception) -> str:
- """SECURITY FIX: Sanitize validation errors to prevent disclosure."""
+def _sanitize_validation_error(error: Exception, log_original: bool = True) -> str:
+ """SECURITY FIX: Sanitize validation errors to prevent disclosure.
+
+ Args:
+ error: The original exception to sanitize for client-facing output.
+ log_original: When True (default), log the original (unsanitized)
+ error server-side at INFO level before returning the sanitized
+ version. This preserves full diagnostics for operators while the
+ client only ever receives the sanitized message. Set to False to
+ suppress the server-side log (e.g. when the caller already logged).
+ """
+ if log_original:
+ # Sanitize control characters before logging to prevent log-line injection.
+ safe_error = sanitize_for_log(error)
+ logger.info(
+ "Sanitizing validation error (%s): %s",
+ type(error).__name__,
+ safe_error,
+ )
+
error_str = str(error)
# Pydantic tagged-union errors prefix the message with a long
diff --git a/tests/unit_tests/mcp_service/utils/test_error_sanitization.py b/tests/unit_tests/mcp_service/utils/test_error_sanitization.py
new file mode 100644
index 00000000000..d0ee3bd4fec
--- /dev/null
+++ b/tests/unit_tests/mcp_service/utils/test_error_sanitization.py
@@ -0,0 +1,70 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for MCP validation-error sanitization."""
+
+import logging
+
+from superset.mcp_service.utils.error_sanitization import _sanitize_validation_error
+
+
+def test_sanitize_redacts_table_name():
+ error = ValueError("Invalid reference to table users in query")
+ # Pure content check — suppress the server-side INFO log of the original.
+ result = _sanitize_validation_error(error, log_original=False)
+ assert "users" not in result
+ assert "[REDACTED]" in result
+
+
+def test_sanitize_logs_original_by_default(caplog):
+ """The original (unsanitized) error is logged server-side before sanitizing."""
+ error = ValueError("Invalid reference to table secret_revenue in query")
+
+ with caplog.at_level(
+ logging.INFO, logger="superset.mcp_service.utils.error_sanitization"
+ ):
+ result = _sanitize_validation_error(error)
+
+ # The client-facing result is sanitized (no raw table name)
+ assert "secret_revenue" not in result
+
+ # The server-side INFO log retains the original, unsanitized detail
+ info_messages = [r.message for r in caplog.records if r.levelno == logging.INFO]
+ assert any("secret_revenue" in m for m in info_messages), (
+ "Original error should be logged server-side before sanitizing"
+ )
+
+
+def test_sanitize_can_suppress_original_log(caplog):
+ """log_original=False suppresses the server-side log of the original error."""
+ error = ValueError("Invalid reference to table secret_revenue in query")
+
+ with caplog.at_level(
+ logging.INFO, logger="superset.mcp_service.utils.error_sanitization"
+ ):
+ _sanitize_validation_error(error, log_original=False)
+
+ info_messages = [r.message for r in caplog.records if r.levelno == logging.INFO]
+ assert not any("secret_revenue" in m for m in info_messages)
+
+
+def test_sanitize_does_not_change_client_output_with_logging():
+ """log_original must not affect the sanitized client-facing output."""
+ error = ValueError("Validation failed due to a timeout")
+ assert _sanitize_validation_error(
+ error, log_original=True
+ ) == _sanitize_validation_error(error, log_original=False)