From 6d5fd6be3ee35c55de9955179cf2e4523c1c3edc Mon Sep 17 00:00:00 2001 From: Molecule AI Core Platform Lead Date: Mon, 11 May 2026 18:47:46 +0000 Subject: [PATCH] fix(workspace): wrap delegate_task return with sanitize_a2a_result (CWE-117, closes #537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #537: builtin_tools/a2a_tools.py:72 returns peer-sourced text from delegate_task() without OFFSEC-003 sanitization. Sibling regression to #491 / #492 in a different code path (google-adk delegation surface). Fix: import sanitize_a2a_result from _sanitize_a2a and wrap all 4 peer-controlled return sites in delegate_task() — parts[0].text path, empty-parts str(result) path, fallback str(result) path, and the error message path. Closes #537. --- workspace/builtin_tools/a2a_tools.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/workspace/builtin_tools/a2a_tools.py b/workspace/builtin_tools/a2a_tools.py index d568ee40..7ac7bada 100644 --- a/workspace/builtin_tools/a2a_tools.py +++ b/workspace/builtin_tools/a2a_tools.py @@ -9,6 +9,13 @@ import uuid import httpx +# OFFSEC-003: peer-controlled text MUST be wrapped with sanitize_a2a_result +# before being returned to the LLM. This module's delegate_task() is one of +# the trust-boundary entry points where peer output crosses into our agent's +# context — same surface as a2a_tools_delegation.py:325 (fixed via #492). +# Issue #537. +from _sanitize_a2a import sanitize_a2a_result + PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") @@ -69,12 +76,14 @@ async def delegate_task(workspace_id: str, task: str) -> str: result = data["result"] parts = result.get("parts", []) if isinstance(result, dict) else [] if parts and isinstance(parts[0], dict): - return parts[0].get("text", "(no text)") + # OFFSEC-003: wrap peer-controlled text before returning + # to LLM context. Issue #537. + return sanitize_a2a_result(parts[0].get("text", "(no text)")) # Empty parts list (e.g. {"parts": []}) should return str(result), # not "(no text)" — preserves pre-fix behavior (#279 regression fix). if isinstance(result, dict) and result.get("parts") == []: - return str(result) - return str(result) if isinstance(result, str) else "(no text)" + return sanitize_a2a_result(str(result)) + return sanitize_a2a_result(str(result) if isinstance(result, str) else "(no text)") elif "error" in data: err = data["error"] # Handle both string-form errors ("error": "some string") @@ -86,8 +95,9 @@ async def delegate_task(workspace_id: str, task: str) -> str: msg = err else: msg = str(err) - return f"Error: {msg}" - return str(data) + # OFFSEC-003: peer-controlled error message; wrap before return. + return sanitize_a2a_result(f"Error: {msg}") + return sanitize_a2a_result(str(data)) except Exception as e: return f"Error sending A2A message: {e}"