diff --git a/superset/common/query_context.py b/superset/common/query_context.py index 40e329113..499240586 100644 --- a/superset/common/query_context.py +++ b/superset/common/query_context.py @@ -18,7 +18,6 @@ from datetime import datetime, timedelta import logging import pickle as pkl -import traceback from typing import Dict, List import numpy as np @@ -199,7 +198,7 @@ class QueryContext: if not error_message: error_message = "{}".format(e) status = utils.QueryStatus.FAILED - stacktrace = traceback.format_exc() + stacktrace = utils.get_stacktrace() if is_loaded and cache_key and cache and status != utils.QueryStatus.FAILED: try: diff --git a/superset/config.py b/superset/config.py index 4261b5464..7679d204d 100644 --- a/superset/config.py +++ b/superset/config.py @@ -102,7 +102,9 @@ WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log"] DEBUG = os.environ.get("FLASK_ENV") == "development" FLASK_USE_RELOAD = True -# Whether to show the stacktrace on 500 error +# Superset allows server-side python stacktraces to be surfaced to the +# user when this feature is on. This may has security implications +# and it's more secure to turn it off in production settings. SHOW_STACKTRACE = True # Extract and use X-Forwarded-For/X-Forwarded-Proto headers? @@ -309,10 +311,8 @@ DEFAULT_MODULE_DS_MAP = OrderedDict( ADDITIONAL_MODULE_DS_MAP = {} ADDITIONAL_MIDDLEWARE = [] -""" -1) https://docs.python-guide.org/writing/logging/ -2) https://docs.python.org/2/library/logging.config.html -""" +# 1) https://docs.python-guide.org/writing/logging/ +# 2) https://docs.python.org/2/library/logging.config.html # Console Log Settings @@ -404,10 +404,8 @@ class CeleryConfig(object): CELERY_CONFIG = CeleryConfig -""" # Set celery config to None to disable all the above configuration -CELERY_CONFIG = None -""" +# CELERY_CONFIG = None # Additional static HTTP headers to be served by your Superset server. Note # Flask-Talisman aplies the relevant security HTTP headers. diff --git a/superset/utils/core.py b/superset/utils/core.py index 6e50db62f..3ad1c622a 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -32,6 +32,7 @@ import signal import smtplib import sys from time import struct_time +import traceback from typing import List, NamedTuple, Optional, Tuple from urllib.parse import unquote_plus import uuid @@ -41,7 +42,7 @@ import bleach import celery from dateutil.parser import parse from dateutil.relativedelta import relativedelta -from flask import flash, Flask, g, Markup, render_template +from flask import current_app, flash, Flask, g, Markup, render_template from flask_appbuilder.security.sqla.models import User from flask_babel import gettext as __ from flask_babel import lazy_gettext as _ @@ -1185,3 +1186,8 @@ def shortid() -> str: class DatasourceName(NamedTuple): table: str schema: str + + +def get_stacktrace(): + if current_app.config.get("SHOW_STACKTRACE"): + return traceback.format_exc() diff --git a/superset/views/base.py b/superset/views/base.py index a77cf517f..c82db76a7 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -66,8 +66,7 @@ def get_error_msg(): def json_error_response(msg=None, status=500, stacktrace=None, payload=None, link=None): if not payload: payload = {"error": "{}".format(msg)} - if stacktrace and conf.get("SHOW_STACKTRACE"): - payload["stacktrace"] = stacktrace + payload["stacktrace"] = utils.get_stacktrace() if link: payload["link"] = link @@ -125,20 +124,20 @@ def handle_api_exception(f): return json_error_response( utils.error_msg_from_exception(e), status=e.status, - stacktrace=traceback.format_exc(), + stacktrace=utils.get_stacktrace(), link=e.link, ) except SupersetException as e: logging.exception(e) return json_error_response( utils.error_msg_from_exception(e), - stacktrace=traceback.format_exc(), + stacktrace=utils.get_stacktrace(), status=e.status, ) except Exception as e: logging.exception(e) return json_error_response( - utils.error_msg_from_exception(e), stacktrace=traceback.format_exc() + utils.error_msg_from_exception(e), stacktrace=utils.get_stacktrace() ) return functools.update_wrapper(wraps, f) diff --git a/superset/views/core.py b/superset/views/core.py index 749812f42..b0dfda988 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -21,7 +21,6 @@ import inspect import logging import os import re -import traceback from typing import Dict, List # noqa: F401 from urllib import parse @@ -3261,11 +3260,9 @@ class Superset(BaseSupersetView): return self.json_response(schemas_allowed_processed) except Exception: return json_error_response( - ( - "Failed to fetch schemas allowed for csv upload in this database! " - "Please contact Superset Admin!\n\n" - "The error message returned was:\n{}" - ).format(traceback.format_exc()) + "Failed to fetch schemas allowed for csv upload in this database! " + "Please contact Superset Admin!", + stacktrace=utils.get_stacktrace(), ) diff --git a/superset/viz.py b/superset/viz.py index f802787ad..5498becec 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -31,7 +31,6 @@ import logging import math import pickle as pkl import re -import traceback import uuid from dateutil import relativedelta as rdelta @@ -423,7 +422,7 @@ class BaseViz(object): if not self.error_message: self.error_message = "{}".format(e) self.status = utils.QueryStatus.FAILED - stacktrace = traceback.format_exc() + stacktrace = utils.get_stacktrace() if ( is_loaded diff --git a/tests/utils_tests.py b/tests/utils_tests.py index 4361682e3..be2ce7952 100644 --- a/tests/utils_tests.py +++ b/tests/utils_tests.py @@ -22,12 +22,14 @@ import uuid import numpy +from superset import app from superset.exceptions import SupersetException from superset.utils.core import ( base_json_conv, convert_legacy_filters_into_adhoc, datetime_f, get_since_until, + get_stacktrace, json_int_dttm_ser, json_iso_dttm_ser, JSONEncodedDict, @@ -763,3 +765,19 @@ class UtilsTestCase(unittest.TestCase): def test_parse_js_uri_path_items_item_optional(self): self.assertIsNone(parse_js_uri_path_item(None)) self.assertIsNotNone(parse_js_uri_path_item("item")) + + def test_get_stacktrace(self): + with app.app_context(): + app.config["SHOW_STACKTRACE"] = True + try: + raise Exception("NONONO!") + except Exception: + stacktrace = get_stacktrace() + self.assertIn("NONONO", stacktrace) + + app.config["SHOW_STACKTRACE"] = False + try: + raise Exception("NONONO!") + except Exception: + stacktrace = get_stacktrace() + assert stacktrace is None