2 Commits

Author SHA1 Message Date
Chuck
7f95c76c18 fix(security): fix plugin update logging and config validation leak
- update_plugin: change logger.exception to logger.error in non-except
  branch (logger.exception outside an except block logs useless
  "NoneType: None" traceback)
- update_plugin: remove duplicate logger.exception call in except block
  (was logging the same failure twice)
- save_plugin_config validation: stop logging full plugin_config dict
  (can contain API keys, passwords, tokens) and raw form_data values;
  log only keys and validation errors instead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:39:20 -04:00
Chuck
bd124596be fix(security): correct error inference, remove debug log leak, consolidate config handlers
- _infer_error_code: map Config* exceptions to CONFIG_LOAD_FAILED
  (ConfigError is only raised by load_config(), so CONFIG_SAVE_FAILED
  produced wrong safe message and wrong suggested_fixes)
- Remove leftover DEBUG logs in save_main_config that dumped full
  request body and all HTTP headers (Authorization, Cookie, etc.)
- Replace dead FileNotFoundError/JSONDecodeError/IOError handlers in
  get_dim_schedule_config with single ConfigError catch (load_config
  already wraps these into ConfigError)
- Remove redundant local `from src.exceptions import ConfigError`
  imports now covered by top-level import
- Strip str(e) from client-facing error messages in dim schedule handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:17:46 -04:00
2 changed files with 19 additions and 37 deletions

View File

@@ -254,7 +254,7 @@ class WebInterfaceError:
exception_name = type(exception).__name__
if "Config" in exception_name:
return ErrorCode.CONFIG_SAVE_FAILED
return ErrorCode.CONFIG_LOAD_FAILED
elif "Plugin" in exception_name:
return ErrorCode.PLUGIN_LOAD_FAILED
elif "Permission" in exception_name or "Access" in exception_name:

View File

@@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
# Import new infrastructure
from src.web_interface.api_helpers import success_response, error_response, validate_request_json
from src.web_interface.errors import ErrorCode
from src.exceptions import ConfigError
from src.plugin_system.operation_types import OperationType
from src.web_interface.logging_config import log_plugin_operation, log_config_change
from src.web_interface.validators import (
@@ -440,32 +441,18 @@ def get_dim_schedule_config():
})
return success_response(data=dim_schedule_config)
except FileNotFoundError as e:
logger.error(f"[DIM SCHEDULE] Config file not found: {e}", exc_info=True)
except ConfigError as e:
logger.error(f"[DIM SCHEDULE] Config error: {e}", exc_info=True)
return error_response(
ErrorCode.CONFIG_LOAD_FAILED,
"Configuration file not found",
status_code=500
)
except json.JSONDecodeError as e:
logger.error(f"[DIM SCHEDULE] Invalid JSON in config file: {e}", exc_info=True)
return error_response(
ErrorCode.CONFIG_LOAD_FAILED,
"Configuration file contains invalid JSON",
status_code=500
)
except (IOError, OSError) as e:
logger.error(f"[DIM SCHEDULE] Error reading config file: {e}", exc_info=True)
return error_response(
ErrorCode.CONFIG_LOAD_FAILED,
f"Error reading configuration file: {str(e)}",
"Configuration file not found or invalid",
status_code=500
)
except Exception as e:
logger.error(f"[DIM SCHEDULE] Unexpected error loading config: {e}", exc_info=True)
return error_response(
ErrorCode.CONFIG_LOAD_FAILED,
f"Unexpected error loading dim schedule configuration: {str(e)}",
"Unexpected error loading dim schedule configuration",
status_code=500
)
@@ -654,10 +641,6 @@ def save_main_config():
if not data:
return jsonify({'status': 'error', 'message': 'No data provided'}), 400
logger.error(f"DEBUG: save_main_config received data: {data}")
logger.error(f"DEBUG: Content-Type header: {request.content_type}")
logger.error(f"DEBUG: Headers: {dict(request.headers)}")
# Merge with existing config (similar to original implementation)
current_config = api_v3.config_manager.load_config()
@@ -1012,7 +995,6 @@ 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:
from src.exceptions import ConfigError
logger.exception("[RawConfig] Failed to save raw main config")
if isinstance(e, ConfigError):
return error_response(
@@ -1051,7 +1033,6 @@ 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:
from src.exceptions import ConfigError
logger.exception("[RawSecrets] Failed to save raw secrets config")
if isinstance(e, ConfigError):
return error_response(
@@ -2738,8 +2719,8 @@ def update_plugin():
}
)
logger.exception("[PluginUpdate] Update failed for %s: %s", plugin_id, error_msg)
logger.error("[PluginUpdate] Update failed for %s: %s", plugin_id, error_msg)
return error_response(
ErrorCode.PLUGIN_UPDATE_FAILED,
error_msg,
@@ -2748,8 +2729,7 @@ def update_plugin():
except Exception as e:
logger.exception("[PluginUpdate] Exception in update_plugin endpoint")
logger.exception("[PluginUpdate] Unhandled exception")
from src.web_interface.errors import WebInterfaceError
error = WebInterfaceError.from_exception(e, ErrorCode.PLUGIN_UPDATE_FAILED)
if api_v3.operation_history:
@@ -4585,15 +4565,17 @@ def save_plugin_config():
if not is_valid:
# Log validation errors for debugging
logger.error(f"Config validation failed for {plugin_id}")
logger.error(f"Validation errors: {validation_errors}")
logger.error(f"Config that failed: {plugin_config}")
logger.error(f"Schema properties: {list(enhanced_schema.get('properties', {}).keys())}")
# Log raw form data if this was a form submission
logger.error(
"[PluginConfig] Validation errors: %s | config keys: %s | schema keys: %s",
validation_errors,
list(plugin_config.keys()),
list(enhanced_schema.get('properties', {}).keys()),
)
if 'application/json' not in (request.content_type or ''):
form_data = request.form.to_dict()
logger.error("[PluginConfig] Raw form data: %s", json.dumps({k: str(v)[:200] for k, v in form_data.items()}, indent=2))
logger.error("[PluginConfig] Parsed config: %s", json.dumps(plugin_config, indent=2, default=str))
logger.error(
"[PluginConfig] Form field keys: %s",
list(request.form.keys()),
)
return error_response(
ErrorCode.CONFIG_VALIDATION_FAILED,
'Configuration validation failed',