From 99097423716ecf2b85985e1e82e8fedfad6de256 Mon Sep 17 00:00:00 2001 From: xr843 <137012659+xr843@users.noreply.github.com> Date: Tue, 5 May 2026 14:22:50 +0800 Subject: [PATCH 1/4] fix(security): enforce tenant scoping on app trace-config endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /console/api/apps//trace-config endpoints (GET/POST/PATCH/ DELETE) only checked that the caller was authenticated; they did not verify that the target app_id belonged to the caller's tenant. A logged-in user from tenant A could read, modify, or delete the tracing configuration of an app in tenant B (e.g., redirect outbound traces to an attacker-controlled Langfuse endpoint). Apply the established @get_app_model decorator (api/controllers/ console/app/wraps.py) to all four verbs. The decorator loads the App with App.tenant_id == current_tenant_id and raises AppNotFoundError on mismatch — same pattern used by mcp_server.py and workflow_trigger.py. Refs: GHSA-48xc-wmw8-3jr3 (reported by zafido via Huntr). --- api/controllers/console/app/ops_trace.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/api/controllers/console/app/ops_trace.py b/api/controllers/console/app/ops_trace.py index cbcf513162..2ae84c3f32 100644 --- a/api/controllers/console/app/ops_trace.py +++ b/api/controllers/console/app/ops_trace.py @@ -7,8 +7,10 @@ from werkzeug.exceptions import BadRequest from controllers.console import console_ns from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist +from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required from libs.login import login_required +from models import App from services.ops_service import OpsService DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" @@ -49,11 +51,12 @@ class TraceAppConfigApi(Resource): @setup_required @login_required @account_initialization_required - def get(self, app_id): + @get_app_model + def get(self, app_model: App): args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore try: - trace_config = OpsService.get_tracing_app_config(app_id=app_id, tracing_provider=args.tracing_provider) + trace_config = OpsService.get_tracing_app_config(app_id=app_model.id, tracing_provider=args.tracing_provider) if not trace_config: return {"has_not_configured": True} return trace_config @@ -71,13 +74,14 @@ class TraceAppConfigApi(Resource): @setup_required @login_required @account_initialization_required - def post(self, app_id): + @get_app_model + def post(self, app_model: App): """Create a new trace app configuration""" args = TraceConfigPayload.model_validate(console_ns.payload) try: result = OpsService.create_tracing_app_config( - app_id=app_id, tracing_provider=args.tracing_provider, tracing_config=args.tracing_config + app_id=app_model.id, tracing_provider=args.tracing_provider, tracing_config=args.tracing_config ) if not result: raise TracingConfigIsExist() @@ -96,13 +100,14 @@ class TraceAppConfigApi(Resource): @setup_required @login_required @account_initialization_required - def patch(self, app_id): + @get_app_model + def patch(self, app_model: App): """Update an existing trace app configuration""" args = TraceConfigPayload.model_validate(console_ns.payload) try: result = OpsService.update_tracing_app_config( - app_id=app_id, tracing_provider=args.tracing_provider, tracing_config=args.tracing_config + app_id=app_model.id, tracing_provider=args.tracing_provider, tracing_config=args.tracing_config ) if not result: raise TracingConfigNotExist() @@ -119,12 +124,13 @@ class TraceAppConfigApi(Resource): @setup_required @login_required @account_initialization_required - def delete(self, app_id): + @get_app_model + def delete(self, app_model: App): """Delete an existing trace app configuration""" args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore try: - result = OpsService.delete_tracing_app_config(app_id=app_id, tracing_provider=args.tracing_provider) + result = OpsService.delete_tracing_app_config(app_id=app_model.id, tracing_provider=args.tracing_provider) if not result: raise TracingConfigNotExist() return {"result": "success"}, 204 From 11c8a4bfa810d9f8c4209eb64a36e1fb0056b43c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 06:25:23 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- api/controllers/console/app/ops_trace.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/app/ops_trace.py b/api/controllers/console/app/ops_trace.py index 2ae84c3f32..9ea1a8325b 100644 --- a/api/controllers/console/app/ops_trace.py +++ b/api/controllers/console/app/ops_trace.py @@ -56,7 +56,9 @@ class TraceAppConfigApi(Resource): args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore try: - trace_config = OpsService.get_tracing_app_config(app_id=app_model.id, tracing_provider=args.tracing_provider) + trace_config = OpsService.get_tracing_app_config( + app_id=app_model.id, tracing_provider=args.tracing_provider + ) if not trace_config: return {"has_not_configured": True} return trace_config From 16b98ea4da1d983b91e75071b730603fa9192d47 Mon Sep 17 00:00:00 2001 From: xr843 <137012659+xr843@users.noreply.github.com> Date: Tue, 5 May 2026 17:49:34 +0800 Subject: [PATCH 3/4] fix(security): also tenant-scope /apps//trace AppTraceApi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AppTraceApi GET/POST endpoints in app.py have the same missing- tenant-check bug as ops_trace.py, on the same advisory. Apply the same @get_app_model decorator pattern here too. Bundled into this PR per zafido's draft patch — same advisory, same bug class, same fix pattern. Refs: GHSA-48xc-wmw8-3jr3 Co-Authored-By: Ido Shani --- api/controllers/console/app/app.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 58ed6efc14..05b8aeceaa 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -840,10 +840,11 @@ class AppTraceApi(Resource): @setup_required @login_required @account_initialization_required - def get(self, app_id): + @get_app_model + def get(self, app_model): """Get app trace""" with session_factory.create_session() as session: - app_trace_config = OpsTraceManager.get_app_tracing_config(app_id, session) + app_trace_config = OpsTraceManager.get_app_tracing_config(app_model.id, session) return app_trace_config @@ -857,12 +858,13 @@ class AppTraceApi(Resource): @login_required @account_initialization_required @edit_permission_required - def post(self, app_id): + @get_app_model + def post(self, app_model): # add app trace args = AppTracePayload.model_validate(console_ns.payload) OpsTraceManager.update_app_tracing_config( - app_id=app_id, + app_id=app_model.id, enabled=args.enabled, tracing_provider=args.tracing_provider, ) From 7d9e1244a742ba36e799ee59e14136551646d5e4 Mon Sep 17 00:00:00 2001 From: xr843 <137012659+xr843@users.noreply.github.com> Date: Sat, 9 May 2026 12:37:09 +0800 Subject: [PATCH 4/4] test: update TraceAppConfigApi tests to pass app_model after tenant-scoping The tenant-scoping change replaced the (self, app_id) signature with (self, app_model) via @get_app_model. Match the pattern used by other tests in this file (MagicMock(id="app-1")). --- .../controllers/console/app/test_app_apis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py index bb737754a1..4263c0b0fa 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_app_apis.py @@ -289,7 +289,7 @@ class TestOpsTraceEndpoints: ) with app.test_request_context("/?tracing_provider=langfuse"): - result = method(app_id="app-1") + result = method(app_model=MagicMock(id="app-1")) assert result == {"has_not_configured": True} @@ -308,7 +308,7 @@ class TestOpsTraceEndpoints: json={"tracing_provider": "langfuse", "tracing_config": {"api_key": "k"}}, ): with pytest.raises(BadRequest): - method(app_id="app-1") + method(app_model=MagicMock(id="app-1")) def test_trace_app_config_delete_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch): api = ops_trace_module.TraceAppConfigApi() @@ -322,7 +322,7 @@ class TestOpsTraceEndpoints: with app.test_request_context("/?tracing_provider=langfuse"): with pytest.raises(BadRequest): - method(app_id="app-1") + method(app_model=MagicMock(id="app-1")) class TestSiteEndpoints: