fix(security): stop leaking Python tracebacks to HTTP clients (#283)

* 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 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: 5ymb01 <noreply@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
5ymb01
2026-03-20 14:51:05 -04:00
committed by GitHub
parent f0dc094cd6
commit f718305886

View File

@@ -406,14 +406,11 @@ def save_schedule_config():
return success_response(message='Schedule configuration saved successfully') return success_response(message='Schedule configuration saved successfully')
except Exception as e: except Exception as e:
import logging logger.exception("[ScheduleConfig] Failed to save schedule configuration")
import traceback
error_msg = f"Error saving schedule config: {str(e)}\n{traceback.format_exc()}"
logging.error(error_msg)
return error_response( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
f"Error saving schedule configuration: {str(e)}", "Error saving schedule configuration",
details=traceback.format_exc(), details="Internal server error - check server logs",
status_code=500 status_code=500
) )
@@ -627,14 +624,11 @@ def save_dim_schedule_config():
return success_response(message='Dim schedule configuration saved successfully') return success_response(message='Dim schedule configuration saved successfully')
except Exception as e: except Exception as e:
import logging logger.exception("[DimScheduleConfig] Failed to save dim schedule configuration")
import traceback
error_msg = f"Error saving dim schedule config: {str(e)}\n{traceback.format_exc()}"
logging.error(error_msg)
return error_response( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
f"Error saving dim schedule configuration: {str(e)}", "Error saving dim schedule configuration",
details=traceback.format_exc(), details="Internal server error - check server logs",
status_code=500 status_code=500
) )
@@ -978,14 +972,11 @@ def save_main_config():
return success_response(message='Configuration saved successfully') return success_response(message='Configuration saved successfully')
except Exception as e: except Exception as e:
import logging logger.exception("[Config] Failed to save configuration")
import traceback
error_msg = f"Error saving config: {str(e)}\n{traceback.format_exc()}"
logging.error(error_msg)
return error_response( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
f"Error saving configuration: {e}", "Error saving configuration",
details=traceback.format_exc(), details="Internal server error - check server logs",
status_code=500 status_code=500
) )
@@ -1021,32 +1012,20 @@ def save_raw_main_config():
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return jsonify({'status': 'error', 'message': f'Invalid JSON: {str(e)}'}), 400 return jsonify({'status': 'error', 'message': f'Invalid JSON: {str(e)}'}), 400
except Exception as e: except Exception as e:
import logging
import traceback
from src.exceptions import ConfigError from src.exceptions import ConfigError
logger.exception("[RawConfig] Failed to save raw main config")
# 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
if isinstance(e, ConfigError): 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( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
error_message, "Error saving raw main configuration",
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 status_code=500
) )
else: else:
error_message = str(e) if str(e) else "An unexpected error occurred while saving the configuration"
return error_response( return error_response(
ErrorCode.UNKNOWN_ERROR, ErrorCode.UNKNOWN_ERROR,
error_message, "An unexpected error occurred while saving the configuration",
details=traceback.format_exc(), details="Internal server error - check server logs",
status_code=500 status_code=500
) )
@@ -1072,24 +1051,22 @@ def save_raw_secrets_config():
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return jsonify({'status': 'error', 'message': f'Invalid JSON: {str(e)}'}), 400 return jsonify({'status': 'error', 'message': f'Invalid JSON: {str(e)}'}), 400
except Exception as e: except Exception as e:
import logging
import traceback
from src.exceptions import ConfigError from src.exceptions import ConfigError
logger.exception("[RawSecrets] Failed to save raw secrets config")
# 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
if isinstance(e, ConfigError): if isinstance(e, ConfigError):
# ConfigError has a message attribute and may have context return error_response(
error_message = str(e) ErrorCode.CONFIG_SAVE_FAILED,
if hasattr(e, 'config_path') and e.config_path: "Error saving raw secrets configuration",
error_message = f"{error_message} (config_path: {e.config_path})" details="Internal server error - check server logs",
status_code=500
)
else: else:
error_message = str(e) if str(e) else "An unexpected error occurred while saving the configuration" return error_response(
ErrorCode.UNKNOWN_ERROR,
return jsonify({'status': 'error', 'message': error_message}), 500 "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']) @api_v3.route('/system/status', methods=['GET'])
def get_system_status(): def get_system_status():
@@ -1470,10 +1447,8 @@ def execute_system_action():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() logger.exception("[SystemAction] Unexpected error")
print(f"Error in execute_system_action: {str(e)}") return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500
print(error_details)
return jsonify({'status': 'error', 'message': str(e), 'details': error_details}), 500
@api_v3.route('/display/current', methods=['GET']) @api_v3.route('/display/current', methods=['GET'])
def get_display_current(): def get_display_current():
@@ -1882,10 +1857,8 @@ def get_installed_plugins():
return jsonify({'status': 'success', 'data': {'plugins': plugins}}) return jsonify({'status': 'success', 'data': {'plugins': plugins}})
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() logger.exception("[Plugins] Error listing installed plugins")
print(f"Error in get_installed_plugins: {str(e)}") return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500
print(error_details)
return jsonify({'status': 'error', 'message': str(e), 'details': error_details}), 500
@api_v3.route('/plugins/health', methods=['GET']) @api_v3.route('/plugins/health', methods=['GET'])
def get_plugin_health(): def get_plugin_health():
@@ -6068,8 +6041,8 @@ def upload_plugin_asset():
}) })
except Exception as e: except Exception as e:
import traceback logger.exception("[API] Unexpected error")
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500
@api_v3.route('/plugins/of-the-day/json/upload', methods=['POST']) @api_v3.route('/plugins/of-the-day/json/upload', methods=['POST'])
def upload_of_the_day_json(): def upload_of_the_day_json():
@@ -6218,8 +6191,8 @@ def upload_of_the_day_json():
}) })
except Exception as e: except Exception as e:
import traceback logger.exception("[API] Unexpected error")
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500
@api_v3.route('/plugins/of-the-day/json/delete', methods=['POST']) @api_v3.route('/plugins/of-the-day/json/delete', methods=['POST'])
def delete_of_the_day_json(): def delete_of_the_day_json():
@@ -6265,8 +6238,8 @@ def delete_of_the_day_json():
}) })
except Exception as e: except Exception as e:
import traceback logger.exception("[API] Unexpected error")
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500
@api_v3.route('/plugins/<plugin_id>/static/<path:file_path>', methods=['GET']) @api_v3.route('/plugins/<plugin_id>/static/<path:file_path>', methods=['GET'])
def serve_plugin_static(plugin_id, file_path): 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) return Response(content, mimetype=content_type)
except Exception as e: except Exception as e:
import traceback logger.exception("[API] Unexpected error")
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500
@api_v3.route('/plugins/calendar/upload-credentials', methods=['POST']) @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'}) return jsonify({'status': 'success', 'message': 'Image deleted successfully'})
except Exception as e: except Exception as e:
import traceback logger.exception("[API] Unexpected error")
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500
@api_v3.route('/plugins/assets/list', methods=['GET']) @api_v3.route('/plugins/assets/list', methods=['GET'])
def list_plugin_assets(): def list_plugin_assets():
@@ -6513,8 +6486,8 @@ def list_plugin_assets():
return jsonify({'status': 'success', 'data': {'assets': assets}}) return jsonify({'status': 'success', 'data': {'assets': assets}})
except Exception as e: except Exception as e:
import traceback logger.exception("[API] Unexpected error")
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 return jsonify({'status': 'error', 'message': 'Internal server error - check server logs'}), 500
@api_v3.route('/logs', methods=['GET']) @api_v3.route('/logs', methods=['GET'])
def get_logs(): def get_logs():