From 8108757fb9a50bb886d36d5d8f3e8a9bee05fc70 Mon Sep 17 00:00:00 2001 From: dazer1234 Date: Mon, 18 May 2026 16:02:39 +0200 Subject: [PATCH] Redact server proxy upstream errors --- node/server_proxy.py | 39 ++++++++++++++--------- node/tests/test_server_proxy_errors.py | 44 ++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 node/tests/test_server_proxy_errors.py diff --git a/node/server_proxy.py b/node/server_proxy.py index 8aad4ef10..c6ad8af19 100644 --- a/node/server_proxy.py +++ b/node/server_proxy.py @@ -6,7 +6,6 @@ from flask import Flask, request, jsonify import requests -import json from urllib.parse import quote app = Flask(__name__) @@ -22,6 +21,23 @@ def _build_local_api_url(path): safe_path = "/".join(quote(part, safe="") for part in parts) return f"{LOCAL_SERVER}/api/{safe_path}" + +def _relay_upstream_response(response): + """Relay upstream responses without exposing internal 5xx details.""" + if response.status_code >= 500: + app.logger.warning("Upstream server returned %s for proxied request", response.status_code) + return jsonify({'error': 'Upstream server error'}), 502 + + content_type = response.headers.get('Content-Type', '') + if 'application/json' in content_type: + try: + return jsonify(response.json()), response.status_code + except ValueError: + app.logger.warning("Upstream server returned invalid JSON with JSON content type") + return jsonify({'error': 'Invalid upstream JSON response'}), 502 + + return response.text, response.status_code + @app.route('/api/', methods=['GET', 'POST']) def proxy(path): """Forward all API requests to local server""" @@ -43,23 +59,16 @@ def proxy(path): # Forward GET requests response = requests.get(url, timeout=10) - # Return the response from local server - # Safely handle non-JSON responses from upstream - content_type = response.headers.get('Content-Type', '') - if 'application/json' in content_type: - try: - return response.json(), response.status_code - except (ValueError, Exception): - # JSON parse failed, fall back to text - return response.text, response.status_code - else: - # Non-JSON response (e.g., HTML error page), return as-is with text - return response.text, response.status_code + return _relay_upstream_response(response) except requests.exceptions.Timeout: return jsonify({'error': 'Local server timeout'}), 504 - except Exception as e: - return jsonify({'error': str(e)}), 500 + except requests.exceptions.RequestException: + app.logger.warning("Local server request failed", exc_info=True) + return jsonify({'error': 'Local server unavailable'}), 502 + except Exception: + app.logger.exception("Unexpected proxy error") + return jsonify({'error': 'Proxy error'}), 500 @app.route('/status') def status(): diff --git a/node/tests/test_server_proxy_errors.py b/node/tests/test_server_proxy_errors.py new file mode 100644 index 000000000..f943645b5 --- /dev/null +++ b/node/tests/test_server_proxy_errors.py @@ -0,0 +1,44 @@ +import sys +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +NODE_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(NODE_DIR)) + +import server_proxy + + +class ServerProxyErrorTests(unittest.TestCase): + def setUp(self): + server_proxy.app.config["TESTING"] = True + self.client = server_proxy.app.test_client() + + @patch("server_proxy.requests.get") + def test_proxy_exception_does_not_leak_message(self, mock_get): + mock_get.side_effect = RuntimeError("secret filesystem path") + + response = self.client.get("/api/stats") + + self.assertEqual(response.status_code, 500) + body = response.get_json() + self.assertEqual(body, {"error": "Proxy error"}) + self.assertNotIn("secret filesystem path", response.get_data(as_text=True)) + + @patch("server_proxy.requests.get") + def test_upstream_500_body_is_redacted(self, mock_get): + upstream = Mock() + upstream.status_code = 500 + upstream.headers = {"Content-Type": "text/plain"} + upstream.text = "Traceback: database password is hunter2" + mock_get.return_value = upstream + + response = self.client.get("/api/stats") + + self.assertEqual(response.status_code, 502) + self.assertEqual(response.get_json(), {"error": "Upstream server error"}) + self.assertNotIn("hunter2", response.get_data(as_text=True)) + + +if __name__ == "__main__": + unittest.main()