From d46f4f23ee01efa18f3bba384a7630fc80513ff1 Mon Sep 17 00:00:00 2001 From: easonysliu Date: Fri, 13 Mar 2026 21:39:40 +0800 Subject: [PATCH] fix: mask sensitive API keys in config endpoint response The GET /api/config endpoint was returning all configuration values in plaintext, including API keys and passwords. This meant anyone with network access could read every secret stored in the config. Add helper functions that identify sensitive keys (those containing API_KEY, SECRET, or PASSWORD) and replace their values with a masked form that only shows the last 4 characters (e.g. "***abcd"). The POST endpoint response is also masked so secrets are never leaked in any config API response. Co-Authored-By: Claude (claude-opus-4-6) --- app.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 8ad9422f..4e297599 100644 --- a/app.py +++ b/app.py @@ -116,6 +116,36 @@ def _safe_finish(self, *args, **kwargs): # pragma: no cover - 运行时才会 'ANSPIRE_API_KEY' ] +# Substrings that mark a config key as sensitive. Values for these keys will +# be masked in API responses so they are never leaked in plaintext. +_SENSITIVE_KEY_PATTERNS = ('API_KEY', 'SECRET', 'PASSWORD') + + +def _is_sensitive_key(key: str) -> bool: + """Return True if *key* looks like it holds a secret value.""" + upper = key.upper() + return any(pat in upper for pat in _SENSITIVE_KEY_PATTERNS) + + +def _mask_value(value: str) -> str: + """Mask a sensitive value, keeping only the last 4 characters visible.""" + if not value: + return '' + if len(value) <= 4: + return '***' + return '***' + value[-4:] + + +def _mask_config(config: dict) -> dict: + """Return a copy of *config* with sensitive values masked.""" + masked = {} + for key, value in config.items(): + if _is_sensitive_key(key) and value: + masked[key] = _mask_value(value) + else: + masked[key] = value + return masked + def _load_config_module(): """Load or reload the config module to ensure latest values are available.""" @@ -1200,10 +1230,14 @@ def search(): @app.route('/api/config', methods=['GET']) def get_config(): - """Expose selected configuration values to the frontend.""" + """Expose selected configuration values to the frontend. + + Sensitive values (API keys, passwords, secrets) are masked so they are + never returned in plaintext over the network. + """ try: config_values = read_config_values() - return jsonify({'success': True, 'config': config_values}) + return jsonify({'success': True, 'config': _mask_config(config_values)}) except Exception as exc: logger.exception("读取配置失败") return jsonify({'success': False, 'message': f'读取配置失败: {exc}'}), 500 @@ -1227,7 +1261,7 @@ def update_config(): try: write_config_values(updates) updated_config = read_config_values() - return jsonify({'success': True, 'config': updated_config}) + return jsonify({'success': True, 'config': _mask_config(updated_config)}) except Exception as exc: logger.exception("更新配置失败") return jsonify({'success': False, 'message': f'更新配置失败: {exc}'}), 500