From f718305886e8e50db5cb9e7f10d1a503a8631a59 Mon Sep 17 00:00:00 2001 From: 5ymb01 <5ymb01ixm@gmail.com> Date: Fri, 20 Mar 2026 14:51:05 -0400 Subject: [PATCH] fix(security): stop leaking Python tracebacks to HTTP clients (#283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(security): stop leaking Python tracebacks to HTTP clients Replace 13 instances where traceback.format_exc() was sent in API JSON responses (via `details=`, `traceback:`, or `details:` keys). - 5 error_response(details=traceback.format_exc()) → generic message - 6 jsonify({'traceback': traceback.format_exc()}) → removed key - 2 jsonify({'details': error_details}) → logger.error() instead Tracebacks in debug mode (app.py error handlers) are preserved as they are guarded by app.debug and expected during development. Co-Authored-By: 5ymb01 Co-Authored-By: Claude Opus 4.6 * fix(security): sanitize str(e) from client responses, add server-side logging Address CodeRabbit review findings: - Replace str(e) in error_response message fields with generic messages - Replace import logging/traceback + manual format with logger.exception() - Add logger.exception() to 6 jsonify handlers that were swallowing errors - All exception details now logged server-side only, not sent to clients Co-Authored-By: 5ymb01 Co-Authored-By: Claude Opus 4.6 * fix: remove duplicate traceback logging, sanitize secrets config error Address CodeRabbit nitpicks: - Remove manual import logging/traceback + logging.error() that duplicated the logger.exception() call in save_raw_main_config - Apply same fix to save_raw_secrets_config: replace str(e) in client response with generic message, use logger.exception() for server-side Co-Authored-By: 5ymb01 Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com> Co-authored-by: 5ymb01 Co-authored-by: Claude Opus 4.6 --- web_interface/blueprints/api_v3.py | 113 +++++++++++------------------ 1 file changed, 43 insertions(+), 70 deletions(-) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 9b8656a6..fe9c807d 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -406,14 +406,11 @@ def save_schedule_config(): return success_response(message='Schedule configuration saved successfully') except Exception as e: - import logging - import traceback - error_msg = f"Error saving schedule config: {str(e)}\n{traceback.format_exc()}" - logging.error(error_msg) + logger.exception("[ScheduleConfig] Failed to save schedule configuration") return error_response( ErrorCode.CONFIG_SAVE_FAILED, - f"Error saving schedule configuration: {str(e)}", - details=traceback.format_exc(), + "Error saving schedule configuration", + details="Internal server error - check server logs", status_code=500 ) @@ -627,14 +624,11 @@ def save_dim_schedule_config(): return success_response(message='Dim schedule configuration saved successfully') except Exception as e: - import logging - import traceback - error_msg = f"Error saving dim schedule config: {str(e)}\n{traceback.format_exc()}" - logging.error(error_msg) + logger.exception("[DimScheduleConfig] Failed to save dim schedule configuration") return error_response( ErrorCode.CONFIG_SAVE_FAILED, - f"Error saving dim schedule configuration: {str(e)}", - details=traceback.format_exc(), + "Error saving dim schedule configuration", + details="Internal server error - check server logs", status_code=500 ) @@ -978,14 +972,11 @@ def save_main_config(): return success_response(message='Configuration saved successfully') except Exception as e: - import logging - import traceback - error_msg = f"Error saving config: {str(e)}\n{traceback.format_exc()}" - logging.error(error_msg) + logger.exception("[Config] Failed to save configuration") return error_response( ErrorCode.CONFIG_SAVE_FAILED, - f"Error saving configuration: {e}", - details=traceback.format_exc(), + "Error saving configuration", + details="Internal server error - check server logs", status_code=500 ) @@ -1021,32 +1012,20 @@ def save_raw_main_config(): except json.JSONDecodeError as e: return jsonify({'status': 'error', 'message': f'Invalid JSON: {str(e)}'}), 400 except Exception as e: - import logging - import traceback from src.exceptions import ConfigError - - # Log the full error for debugging - error_msg = f"Error saving raw main config: {str(e)}\n{traceback.format_exc()}" - logging.error(error_msg) - - # Extract more specific error message if it's a ConfigError + logger.exception("[RawConfig] Failed to save raw main config") if isinstance(e, ConfigError): - error_message = str(e) - if hasattr(e, 'config_path') and e.config_path: - error_message = f"{error_message} (config_path: {e.config_path})" return error_response( ErrorCode.CONFIG_SAVE_FAILED, - error_message, - details=traceback.format_exc(), - context={'config_path': e.config_path} if hasattr(e, 'config_path') and e.config_path else None, + "Error saving raw main configuration", + details="Internal server error - check server logs", status_code=500 ) else: - error_message = str(e) if str(e) else "An unexpected error occurred while saving the configuration" return error_response( ErrorCode.UNKNOWN_ERROR, - error_message, - details=traceback.format_exc(), + "An unexpected error occurred while saving the configuration", + details="Internal server error - check server logs", status_code=500 ) @@ -1072,24 +1051,22 @@ def save_raw_secrets_config(): except json.JSONDecodeError as e: return jsonify({'status': 'error', 'message': f'Invalid JSON: {str(e)}'}), 400 except Exception as e: - import logging - import traceback from src.exceptions import ConfigError - - # Log the full error for debugging - error_msg = f"Error saving raw secrets config: {str(e)}\n{traceback.format_exc()}" - logging.error(error_msg) - - # Extract more specific error message if it's a ConfigError + logger.exception("[RawSecrets] Failed to save raw secrets config") if isinstance(e, ConfigError): - # ConfigError has a message attribute and may have context - error_message = str(e) - if hasattr(e, 'config_path') and e.config_path: - error_message = f"{error_message} (config_path: {e.config_path})" + return error_response( + ErrorCode.CONFIG_SAVE_FAILED, + "Error saving raw secrets configuration", + details="Internal server error - check server logs", + status_code=500 + ) else: - error_message = str(e) if str(e) else "An unexpected error occurred while saving the configuration" - - return jsonify({'status': 'error', 'message': error_message}), 500 + return error_response( + ErrorCode.UNKNOWN_ERROR, + "An unexpected error occurred while saving the configuration", + details="Internal server error - check server logs", + status_code=500 + ) @api_v3.route('/system/status', methods=['GET']) def get_system_status(): @@ -1470,10 +1447,8 @@ def execute_system_action(): except Exception as e: import traceback - error_details = traceback.format_exc() - print(f"Error in execute_system_action: {str(e)}") - print(error_details) - return jsonify({'status': 'error', 'message': str(e), 'details': error_details}), 500 + logger.exception("[SystemAction] Unexpected error") + return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500 @api_v3.route('/display/current', methods=['GET']) def get_display_current(): @@ -1882,10 +1857,8 @@ def get_installed_plugins(): return jsonify({'status': 'success', 'data': {'plugins': plugins}}) except Exception as e: import traceback - error_details = traceback.format_exc() - print(f"Error in get_installed_plugins: {str(e)}") - print(error_details) - return jsonify({'status': 'error', 'message': str(e), 'details': error_details}), 500 + logger.exception("[Plugins] Error listing installed plugins") + return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500 @api_v3.route('/plugins/health', methods=['GET']) def get_plugin_health(): @@ -6068,8 +6041,8 @@ def upload_plugin_asset(): }) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + logger.exception("[API] Unexpected error") + return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500 @api_v3.route('/plugins/of-the-day/json/upload', methods=['POST']) def upload_of_the_day_json(): @@ -6218,8 +6191,8 @@ def upload_of_the_day_json(): }) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + logger.exception("[API] Unexpected error") + return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500 @api_v3.route('/plugins/of-the-day/json/delete', methods=['POST']) def delete_of_the_day_json(): @@ -6265,8 +6238,8 @@ def delete_of_the_day_json(): }) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + logger.exception("[API] Unexpected error") + return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500 @api_v3.route('/plugins//static/', methods=['GET']) def serve_plugin_static(plugin_id, file_path): @@ -6313,8 +6286,8 @@ def serve_plugin_static(plugin_id, file_path): return Response(content, mimetype=content_type) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + logger.exception("[API] Unexpected error") + return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500 @api_v3.route('/plugins/calendar/upload-credentials', methods=['POST']) @@ -6485,8 +6458,8 @@ def delete_plugin_asset(): return jsonify({'status': 'success', 'message': 'Image deleted successfully'}) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + logger.exception("[API] Unexpected error") + return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500 @api_v3.route('/plugins/assets/list', methods=['GET']) def list_plugin_assets(): @@ -6513,8 +6486,8 @@ def list_plugin_assets(): return jsonify({'status': 'success', 'data': {'assets': assets}}) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + logger.exception("[API] Unexpected error") + return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500 @api_v3.route('/logs', methods=['GET']) def get_logs():