T02 · Agent Memory Poisoning
Error
- Location
- ui/server.py:128
- Finding
- Unauthenticated Local API Allows Persistent Workflow Instruction Poisoning<![CDATA[ ## Vulnerability Details **File Location**: `ui/server.py:128-178`, `ui/server.py:257-268`, and `scripts/workflow_engine.py:194-225` **Vulnerability Type**: Unauthenticated state-changing API, CSRF, and persistent Agent memory poisoning **Risk Level**: High ### Vulnerable Code The server exposes a state-changing workflow endpoint without authentication, CSRF protection, or request-origin validation: ```python # ui/server.py:128-139 def do_POST(self) -> None: parsed = urlparse(self.path) path = unquote(parsed.path) if path == "/api/workflows": return self.handle_save_workflow() if path == "/api/match": return self.handle_match_workflows() if path == "/api/capture-draft": return self.handle_capture_draft() if path == "/api/render-brief": return self.handle_render_brief() self.respond_error(HTTPStatus.NOT_FOUND, "Unknown endpoint.") ``` Attacker-supplied workflow content is validated only for a name and syntactically safe identifier, then written directly to the workflow library: ```python # ui/server.py:158-178 def handle_save_workflow(self) -> None: body = self.read_json_body() if body is None: return original_id = body.pop("_original_id", None) try: workflow = validate_workflow(body) destination = workflow_path(self.config.workflows_dir, workflow["id"]) except ValueError as exc: return self.respond_error(HTTPStatus.BAD_REQUEST, str(exc)) if original_id and original_id != workflow["id"]: try: old_path = workflow_path(self.config.workflows_dir, original_id) except ValueError as exc: return self.respond_error(HTTPStatus.BAD_REQUEST, str(exc)) if old_path.exists(): old_path.unlink() destination.write_text(json.dumps(workflow, indent=2) + "\n", encoding="utf-8") self.respond_json({"ok": True, "id": workflow["id"]}) ``` The request parser accepts JSON without requiring ...[truncated 6119 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Require authentication for all API endpoints** - Generate a cryptographically random token when the server starts. - Require the token in a custom request header for every API request. - Avoid placing long-lived credentials in workflow files or URLs. - Compare tokens using a constant-time comparison function. 2. **Implement CSRF protection** - Generate a per-session CSRF token. - Require it on every POST, PUT, PATCH, and DELETE request. - Reject state-changing requests without a valid token. 3. **Validate request origins** - Check the `Origin` header against the exact expected local UI origin. - Reject unexpected, missing, or opaque origins for state-changing requests. - Validate the `Host` header to reduce DNS rebinding exposure. - Do not use CORS response headers as a substitute for authentication. 4. **Enforce strict request formats** - Require `Content-Type: application/json`. - Reject simple cross-origin content types such as `text/plain`. - Validate the complete workflow against a strict schema. - Reject unknown fields such as `_original_id` unless they are explicitly expected and authorized. 5. **Limit request sizes** - Configure a small maximum request-body size appropriate for workflow JSON. - Return HTTP 413 before reading a body whose declared size exceeds the limit. - Limit workflow field lengths and the number of steps, keywords, and tools. 6. **Protect workflow integrity** - Use atomic writes through a temporary file in the same directory followed by a rename. - Refuse silent overwrites unless the client supplies the expected current version or content hash. - Keep revision history or backups so poisoned or accidentally deleted workflows can be restored. - Defend against symlink targets when reading, writing, or deleting workflow files. 7. **Treat stored workflow text as untrusted data** - Add an explicit boundary to generated briefs st ...[truncated 866 chars]
