Skip to content

[Security] Remote Agent-Driven Bash Auto-Execution via Web Chat Input in CowAgent #2874

@YLChen-007

Description

@YLChen-007

Advisory Details

Title: Remote Agent-Driven Bash Auto-Execution via Web Chat Input in CowAgent

Description:

Summary

CowAgent executes model-selected high-impact tools during normal web chat handling without a central runtime approval gate. A remote or authenticated web user can reach the agent execution loop through the standard POST /message interface and cause the built-in bash tool to run shell commands with the CowAgent process user's privileges. This was reproduced end-to-end against the real service entrypoint with the public web channel and confirmed by filesystem side effects.

Details

The issue is in the standard agent-enabled web chat flow. The web channel accepts attacker-controlled message content from POST /message, forwards it into agent mode, and the agent runtime directly dispatches any model-selected tool call. There is no central approval or authorization barrier for dangerous tools such as bash.

The relevant flow is:

POST /message
  -> WebChannel.post_message()
  -> Channel.build_reply_content()
  -> Bridge.fetch_agent_reply()
  -> AgentStreamExecutor._execute_tool()
  -> Bash.execute()
  -> subprocess.run(..., shell=True, ...)

Two design choices combine into the vulnerability:

  1. channel/channel.py enables agent mode by default and forwards requests into Bridge.fetch_agent_reply(...).
  2. agent/protocol/agent_stream.py directly executes the chosen tool with tool.execute_tool(arguments) and does not require explicit user confirmation for dangerous built-in tools.

The built-in bash tool only performs narrow checks against a few obviously sensitive patterns and a small set of extremely dangerous commands. Ordinary shell commands such as creating or modifying files in the workspace are auto-approved and executed with subprocess.run(..., shell=True, ...).

The following excerpts capture the vulnerable behavior:

# channel/channel.py
use_agent = conf().get("agent", True)
...
return Bridge().fetch_agent_reply(
    query=query,
    context=context,
    on_event=on_event,
    clear_history=False
)
# agent/protocol/agent_stream.py
tool = self.tools.get(tool_name)
...
result: ToolResult = tool.execute_tool(arguments)
# agent/tools/bash/bash.py
if self.safety_mode:
    warning = self._get_safety_warning(command)
    if warning:
        return ToolResult.fail(...)
...
result = subprocess.run(
    command,
    shell=True,
    cwd=self.cwd,
    ...
)

This is not a synthetic internal-only proof. The reproduction used the real python3 app.py entrypoint, the standard web channel, POST /message, GET /stream, and a deterministic OpenAI-compatible upstream that returned a bash tool call. The vulnerable run created vuln-canary.txt containing CANARY. A control run used the same HTTP path and environment but replaced the dangerous tool call with ls; in that case no control canary was created.

PoC

Prerequisites

  • Python dependencies installed for the project, including web.py
  • Agent mode enabled
  • Standard web channel enabled
  • A model provider that supports tool calling, or the supplied local OpenAI-compatible test server
  • Write access for the CowAgent process user to the agent workspace or target file path

Reproduction Steps

  1. Download the harness from: http_harness.py
  2. Download the fake upstream model server from: fake_openai_server.py
  3. Download the vulnerable path verification script from: verification_test.py
  4. Download the control script from: control-normal-behavior.py
  5. Run the vulnerable-path verification:
    python3 verification_test.py
  6. Observe that the script starts the real application with a temporary web-channel config, posts a normal message to POST /message, consumes GET /stream, and confirms creation of vuln-canary.txt.
  7. Run the control:
    python3 control-normal-behavior.py
  8. Observe that the same HTTP path completes, but no control-canary.txt is created when the dangerous bash tool call is replaced by a benign ls tool call.

Log of Evidence

Vulnerable run:

Verification mode: End-to-End
Data flow: [HTTP POST /message] -> [WebChannel.post_message] -> [Channel.build_reply_content] -> [Bridge.fetch_agent_reply] -> [AgentStreamExecutor._execute_tool] -> [Bash.execute] -> [filesystem canary]
HTTP liveness: /chat returned status=200 on 127.0.0.1:38979
POST /message: {"status": "success", "request_id": "b52c2227-47bd-448f-a680-ee84285bf3dd", "stream": true}
SSE tool_start count: 1
SSE done event: {"type": "done", "content": "completed", "request_id": "b52c2227-47bd-448f-a680-ee84285bf3dd", ...}
Independent observation: canary_exists=True canary_content='CANARY\n'

Control run:

Verification mode: End-to-End
Control behavior: same HTTP path, dangerous tool input removed from model response
HTTP liveness: /chat returned status=200 on 127.0.0.1:51957
POST /message: {"status": "success", "request_id": "491a84a6-c837-4d90-a77a-775d34a2e836", "stream": true}
SSE tool_start count: 1
SSE done event: {"type": "done", "content": "completed", "request_id": "491a84a6-c837-4d90-a77a-775d34a2e836", ...}
Independent observation: canary_exists=False canary_content=''

Impact

This is a remote code execution / arbitrary command execution issue in the agent runtime. Any deployment exposing the web channel to untrusted or semi-trusted users is affected if those users can reach the agent flow and influence tool selection. Successful exploitation allows command execution and file modification with the CowAgent process user's privileges. In practice this can lead to:

  • Arbitrary file write or overwrite in the agent workspace
  • Modification of automation artifacts, scripts, cached data, or knowledge files
  • Reading or exfiltrating process-accessible local data
  • Further host or network pivoting depending on deployment permissions

Affected products

  • Ecosystem: github
  • Package name: zhayujie/CowAgent
  • Affected versions: <= 2.1.0
  • Patched versions:

Severity

  • Severity: High
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Weaknesses

  • CWE: CWE-862: Missing Authorization

Occurrences

Permalink Description
# Check if agent mode is enabled
use_agent = conf().get("agent", True)
if use_agent:
try:
logger.info("[Channel] Using agent mode")
# Add channel_type to context if not present
if context and "channel_type" not in context:
context["channel_type"] = self.channel_type
# Read on_event callback injected by the channel (e.g. web SSE)
on_event = context.get("on_event") if context else None
# Use agent bridge to handle the query
return Bridge().fetch_agent_reply(
query=query,
context=context,
on_event=on_event,
clear_history=False
)
Agent mode is enabled by default and web/chat requests are forwarded into Bridge.fetch_agent_reply(...) without a dangerous-tool approval checkpoint.

CowAgent/bridge/bridge.py

Lines 182 to 197 in feaa907

def fetch_agent_reply(self, query: str, context: Context = None,
on_event=None, clear_history: bool = False) -> Reply:
"""
Use super agent to handle the query
Args:
query: User query
context: Context object
on_event: Event callback for streaming
clear_history: Whether to clear conversation history
Returns:
Reply object
"""
agent_bridge = self.get_agent_bridge()
return agent_bridge.agent_reply(query, context, on_event, clear_history)
The bridge hands the user request directly into the agent runtime through agent_bridge.agent_reply(...).
# Set tool context
tool.model = self.model
tool.context = self.agent
# Execute tool
start_time = time.time()
result: ToolResult = tool.execute_tool(arguments)
execution_time = time.time() - start_time
result_dict = {
The agent runtime looks up the selected tool and executes tool.execute_tool(arguments) directly, with no central confirmation barrier for dangerous tools.
# Security check: Prevent accessing sensitive config files
if "~/.cow/.env" in command or "~/.cow" in command:
return ToolResult.fail(
"Error: Access denied. API keys and credentials must be accessed through the env_config tool only."
)
# Optional safety check - only warn about extremely dangerous commands
if self.safety_mode:
warning = self._get_safety_warning(command)
if warning:
return ToolResult.fail(
f"Safety Warning: {warning}\n\nIf you believe this command is safe and necessary, please ask the user for confirmation first, explaining what the command does and why it's needed.")
The bash tool only blocks a narrow set of sensitive patterns and extremely dangerous commands; routine file-writing commands remain auto-approved.
result = subprocess.run(
command,
shell=True,
cwd=self.cwd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
env=env,
The approved command is executed with subprocess.run(..., shell=True, ...), producing real command execution and filesystem side effects.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions