From 3853b53dea712bb7f214134077c3dd758e2768e4 Mon Sep 17 00:00:00 2001 From: 5ymb01 <5ymb01@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:57:41 -0400 Subject: [PATCH 1/3] fix(security): stop leaking Python tracebacks to HTTP clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- web_interface/blueprints/api_v3.py | 34 +++++++++++++----------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index fec8eedf2..c57fc4de2 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -413,7 +413,7 @@ def save_schedule_config(): return error_response( ErrorCode.CONFIG_SAVE_FAILED, f"Error saving schedule configuration: {str(e)}", - details=traceback.format_exc(), + details="Internal server error - check server logs", status_code=500 ) @@ -634,7 +634,7 @@ def save_dim_schedule_config(): return error_response( ErrorCode.CONFIG_SAVE_FAILED, f"Error saving dim schedule configuration: {str(e)}", - details=traceback.format_exc(), + details="Internal server error - check server logs", status_code=500 ) @@ -985,7 +985,7 @@ def save_main_config(): return error_response( ErrorCode.CONFIG_SAVE_FAILED, f"Error saving configuration: {e}", - details=traceback.format_exc(), + details="Internal server error - check server logs", status_code=500 ) @@ -1037,7 +1037,7 @@ def save_raw_main_config(): return error_response( ErrorCode.CONFIG_SAVE_FAILED, error_message, - details=traceback.format_exc(), + details="Internal server error - check server logs", context={'config_path': e.config_path} if hasattr(e, 'config_path') and e.config_path else None, status_code=500 ) @@ -1046,7 +1046,7 @@ def save_raw_main_config(): return error_response( ErrorCode.UNKNOWN_ERROR, error_message, - details=traceback.format_exc(), + details="Internal server error - check server logs", status_code=500 ) @@ -1470,10 +1470,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.error(f"Error in execute_system_action: {str(e)}\n{traceback.format_exc()}") + return jsonify({'status': 'error', 'message': str(e)}), 500 @api_v3.route('/display/current', methods=['GET']) def get_display_current(): @@ -1882,10 +1880,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.error(f"Error in get_installed_plugins: {str(e)}\n{traceback.format_exc()}") + return jsonify({'status': 'error', 'message': str(e)}), 500 @api_v3.route('/plugins/health', methods=['GET']) def get_plugin_health(): @@ -6069,7 +6065,7 @@ def upload_plugin_asset(): except Exception as e: import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + return jsonify({'status': 'error', 'message': str(e)}), 500 @api_v3.route('/plugins/of-the-day/json/upload', methods=['POST']) def upload_of_the_day_json(): @@ -6219,7 +6215,7 @@ def upload_of_the_day_json(): except Exception as e: import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + return jsonify({'status': 'error', 'message': str(e)}), 500 @api_v3.route('/plugins/of-the-day/json/delete', methods=['POST']) def delete_of_the_day_json(): @@ -6266,7 +6262,7 @@ def delete_of_the_day_json(): except Exception as e: import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + return jsonify({'status': 'error', 'message': str(e)}), 500 @api_v3.route('/plugins//static/', methods=['GET']) def serve_plugin_static(plugin_id, file_path): @@ -6312,7 +6308,7 @@ def serve_plugin_static(plugin_id, file_path): except Exception as e: import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + return jsonify({'status': 'error', 'message': str(e)}), 500 @api_v3.route('/plugins/calendar/upload-credentials', methods=['POST']) @@ -6484,7 +6480,7 @@ def delete_plugin_asset(): except Exception as e: import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + return jsonify({'status': 'error', 'message': str(e)}), 500 @api_v3.route('/plugins/assets/list', methods=['GET']) def list_plugin_assets(): @@ -6512,7 +6508,7 @@ def list_plugin_assets(): except Exception as e: import traceback - return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 + return jsonify({'status': 'error', 'message': str(e)}), 500 @api_v3.route('/logs', methods=['GET']) def get_logs(): From a06187adc6ae744479b4f0de5ca4b8f793df3f4c Mon Sep 17 00:00:00 2001 From: 5ymb01 <5ymb01@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:43:25 -0400 Subject: [PATCH 2/3] 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 --- web_interface/blueprints/api_v3.py | 63 ++++++++++++------------------ 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index c57fc4de2..22e73f034 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -406,13 +406,10 @@ 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)}", + "Error saving schedule configuration", details="Internal server error - check server logs", status_code=500 ) @@ -627,13 +624,10 @@ 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)}", + "Error saving dim schedule configuration", details="Internal server error - check server logs", status_code=500 ) @@ -978,13 +972,10 @@ 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}", + "Error saving configuration", details="Internal server error - check server logs", status_code=500 ) @@ -1030,22 +1021,18 @@ def save_raw_main_config(): 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, + "Error saving raw main configuration", details="Internal server error - check server logs", - context={'config_path': e.config_path} if hasattr(e, 'config_path') and e.config_path else None, 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, + "An unexpected error occurred while saving the configuration", details="Internal server error - check server logs", status_code=500 ) @@ -1470,8 +1457,8 @@ def execute_system_action(): except Exception as e: import traceback - logger.error(f"Error in execute_system_action: {str(e)}\n{traceback.format_exc()}") - return jsonify({'status': 'error', 'message': str(e)}), 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(): @@ -1880,8 +1867,8 @@ def get_installed_plugins(): return jsonify({'status': 'success', 'data': {'plugins': plugins}}) except Exception as e: import traceback - logger.error(f"Error in get_installed_plugins: {str(e)}\n{traceback.format_exc()}") - return jsonify({'status': 'error', 'message': str(e)}), 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(): @@ -6064,8 +6051,8 @@ def upload_plugin_asset(): }) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e)}), 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(): @@ -6214,8 +6201,8 @@ def upload_of_the_day_json(): }) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e)}), 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(): @@ -6261,8 +6248,8 @@ def delete_of_the_day_json(): }) except Exception as e: - import traceback - return jsonify({'status': 'error', 'message': str(e)}), 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): @@ -6307,8 +6294,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)}), 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']) @@ -6479,8 +6466,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)}), 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(): @@ -6507,8 +6494,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)}), 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(): From d485973bdd28cc520583f3e67631c71b2bbf6b35 Mon Sep 17 00:00:00 2001 From: 5ymb01 <5ymb01@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:58:29 -0400 Subject: [PATCH 3/3] 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 --- web_interface/blueprints/api_v3.py | 36 +++++++++++------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 22e73f034..958232b59 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -1012,15 +1012,7 @@ 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): return error_response( @@ -1059,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():