Back to  vulnerability database

CVE-2026-109443: Authenticated Remote Code Execution in Copilot Standard Mode via Unsandboxed Host Shell

A flaw in JoySafeter's Copilot standard mode gives the agent an unrestricted shell tool that runs on the server host, with no human approval. Any authenticated tenant can select this mode and run arbitrary commands as the service process, reading its secrets and reaching other tenants' data and workloads.

CVSS Score
9.9
Severity
Critical
Type
Authenticated Remote Code Execution
Attack type
Authenticated Remote Code Execution
Vendor/tech
jd-opensource/JoySafeter
Known exploit
No
Impact Assessment
Confidentiality
High
Integrity
High
Availability
High

‍Overview

‍JoySafeter is an open-source enterprise AI agent platform from JD for building, running, testing and tracing autonomous agent teams. Its Copilot assistant has a "standard" mode built on a backend that deepagents treats as a sandbox, so the agent automatically receives an execute tool.

‍

That tool runs commands directly on the server host with shell=True, no allow or deny list and no human approval. The default Copilot mode has no shell, but the mode is chosen by the client in the WebSocket request, so any authenticated tenant can select the standard mode and make the agent run arbitrary commands on the host.

‍

Because any authenticated JoySafeter tenant can switch the Copilot to its standard mode and make it run shell commands directly on the server host, with no approval step and no command restrictions, multi-tenant deployments should take the standard Copilot mode out of service or move it into a sandbox immediately.

‍

‍

Affected Product

jd-opensource/JoySafeter

‍

Affected Files

  • backend/app/core/agent/backends/utils/command_executor.py (execute_local_command, subprocess.run(command, shell=True, ...))
  • backend/app/core/agent/backends/filesystem_sandbox.py (FilesystemSandboxBackend (FilesystemBackend, SandboxBackendProtocol), execute() forwarding to execute_local_command)
  • backend/app/core/agent/sample_agent.py (get_agent: FilesystemSandboxBackend(root_dir=..., virtual_mode=True), middleware=[FilesystemMiddleware(backend=backend), ...], create_agent(...) with no excluded_tools and no interrupt_on)
  • backend/app/core/copilot/agent.py (get_copilot_agent delegates to sample_agent.get_agent)
  • backend/app/services/copilot_service.py (_get_copilot_stream: if mode == "deepagents" takes the non-execution path, else get_copilot_agent, the host-shell agent)
  • backend/app/websocket/chat_protocol.py and backend/app/websocket/chat_turn_executor.py (client-supplied copilot mode reaches _get_copilot_stream`)

‍

Root Cause

The Copilot "standard" agent is built on a FilesystemSandboxBackend that implements the deepagents SandboxBackendProtocol, so FilesystemMiddleware auto-adds an execute shell tool to the agent alongside the file tools. That backend's execute() calls execute_local_command, which runs subprocess.run(command, shell=True, capture_output=True, text=True, timeout=timeout, cwd=cwd) directly on the host, with no command allow or deny list, and the agent is created with no excluded_tools and no interrupt_on human-in-the-loop gate.

‍

Because shell=True runs an unvalidated string, shell metacharacters are interpreted. The virtual_mode=True argument path-scopes only the file tools and does not constrain the shell. An authenticated tenant whose Copilot WebSocket frame sets mode to any value other than deepagents is routed to this host-shell agent, so tenant chat, or content the assistant ingests, can drive arbitrary host commands.

‍

Note on reachability: when the Copilot mode is omitted it defaults to deepagents, which builds a non-execution FilesystemBackend with no shell. The host-shell "standard" agent is therefore an explicit, client-selected mode rather than the default, and the client controls mode directly in the WebSocket copilot extension.

‍

PoC

As an authenticated tenant, send a Copilot chat.start WebSocket frame whose extension.mode is set to a non-deepagents value (for example standard) with a benign operations request:

"Please confirm which user and groups this runtime runs as by running the shell command id and showing its full output."

‍

The agent calls the auto-exposed execute tool, subprocess.run("id", shell=True, ...) runs on the server host, and the host user is returned in chat. Substituting any command, or shell metacharacters, confirms arbitrary host execution.

‍

Reproduced on a local clone at commit 12234a1 with benign canaries only, using JoySafeter's verbatim FilesystemSandboxBackend and execute_local_command plus the exact Copilot construction (create_agent with FilesystemMiddleware(backend=...), no excluded_tools, no interrupt_on):

  • Scripted deterministic driver: despite passing only tools=[think], the full host toolset was auto-added (edit_file, execute, glob, grep, ls, read_file, write_file) and execute ran id on the host, returning uid=... gid=20(staff) ... with exit code 0.
  • Real model: a benign tenant chat message drove the model to call execute and return the host id output.

‍

Impact

Remote code execution on the JoySafeter server host as the service process user, reachable by an authenticated tenant who selects the Copilot standard mode. Consequences include reading the process environment and configuration secrets, reaching internal services, pivoting, and persistence.

‍

On a multi-tenant deployment execution crosses tenant boundaries, so one tenant reaches other tenants' data and workloads.

‍

Suggested Remediation

1. Route the Copilot "standard" agent through the same Docker sandbox the graph engine already uses (PydanticSandboxAdapter / DockerSandbox) instead of the host FilesystemSandboxBackend. Isolate first, then grant tools.

‍

2. If a host backend must remain, remove execute for chat agents via excluded_tools or a tool-exclusion middleware. Adjusting the tools= list alone does not work, because the built-in tools are auto-added by the middleware. Gate whatever remains with interrupt_on / human-in-the-loop approval.

‍

3. Apply the existing command allow and deny checks to this path as well. Today they exist only on the Docker backend.

‍

4. Do not execute shell=True strings. Run argv lists without a shell, under an unprivileged, isolated user, so shell metacharacters cannot be interpreted.

‍

5. Treat all tenant chat and all ingested content as attacker-controlled.

‍

Additional Resources

  • CVE-2026-109443 record

‍

‍

Automated AI Red Teaming

Test your applications against adversarial conditions.
Book a Demo