Is FoundationAgents/MetaGPT safe?
- Python shell/command execution
- Python dynamic code execution
- Possible command injection (shell + dynamic command)
What to do: Read the findings below before installing: each one opens the exact line of code it was found on.
FoundationAgents/MetaGPT is a PyPI package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 2 risky constructs are reported for review. It can: dynamic code execution, filesystem read, filesystem write, install time execution, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 40/100 (medium).
repo
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of FoundationAgents/MetaGPT's authors. Report a false positive.
Behavioral traits
How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.
Findings (10)
The code builds an OS command out of values that can change at runtime, then runs it through a shell.
res = subprocess.run(adb_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
result = subprocess.run(command, shell=True, check=True, cwd=str(output_dir))
result = os.system(check_command)
process = await asyncio.create_subprocess_shell(
" ".join(commands), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)Why it matters: If any of those values come from untrusted input, an attacker can run their own commands on the machine.
Fix: Pass arguments as a list without shell=True (e.g. subprocess.run(['git', 'checkout', branch])); never build a shell string from external input. If a shell is unavoidable, quote with shlex.quote.
It loads data with a format that can rebuild arbitrary objects (e.g. pickle, or unsafe YAML).
return pickle.load(f)
self.root_node = pickle.load(f)
message = pickle.loads(message_ser)
Why it matters: Feeding such a loader untrusted data can execute code hidden inside that data.
Fix: Deserialize untrusted data with a safe format/loader: JSON, or yaml.safe_load / Loader=SafeLoader. Reserve pickle/marshal for data you fully control.
The code turns strings into live code at runtime (eval / new Function / exec).
await eval("(async () => {" + programs + "\n" + code + "})()");Why it matters: If those strings aren't fixed and trusted, they become a way to run arbitrary code.
Fix: Avoid evaluating dynamically constructed code; if unavoidable, ensure the input is a trusted constant and never derived from external data.
The code turns strings into live code at runtime (eval / new Function / exec).
extracted_data[field_name] = eval(raw_value)
extracted_data[field_name] = eval(raw_value)
exec(code, namespace)
groundingdino_model = load_model(file_path, device=device).eval()
exec(code, global_namespace)
exec(test_code, globals())
thoughts = eval(thoughts)
compile(code, fname, "exec") # USE TRACEBACK BELOW HERE
new_mapping[key] = eval(value) # `"'(list[str], Ellipsis)"` to `(list[str], ...)`
Why it matters: If those strings aren't fixed and trusted, they become a way to run arbitrary code.
Fix: Avoid evaluating dynamically constructed code; if unavoidable, ensure the input is a trusted constant and never derived from external data.
The Python build/install configuration runs code at install time.
"""A custom command to run `npm install -g @mermaid-js/mermaid-cli` via a subprocess."""
subprocess.check_call(["npm", "install", "-g", "@mermaid-js/mermaid-cli"])
except subprocess.CalledProcessError as e:
cmdclass={Why it matters: Code that runs during pip install is a common supply-chain execution point.
Fix: Verify the build hook performs only a legitimate build step and does not execute commands or reach the network during installation.
The component can run operating-system commands or spawn processes.
process = subprocess.Popen(
command, cwd=working_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
)return subprocess.run(cmd, check=check, cwd=cwd, env=env)
res = subprocess.run(adb_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
result = subprocess.run(command, shell=True, check=True, cwd=str(output_dir))
process = subprocess.Popen(cmd, cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = subprocess.run(command, cwd=cwd, capture_output=True, text=True, env=env, timeout=timeout, shell=shell)
self.process = await asyncio.create_subprocess_exec(
*self.shell_command,
stdin=PIPE,
stdout=PIPE,
stderr=STDOUT,
executable=self.executable,
env=os.environ.copy(), …process = await asyncio.create_subprocess_exec(
sys.executable,
"-m",
"playwright",
"install",
*browsers,
# "--with-deps",
stdout=asyncio.subprocess.PIPE, …result = os.system(check_command)
from git.repo import Repo
from git.repo.fun import is_git_dir
process = await asyncio.create_subprocess_shell(
" ".join(commands), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)subprocess.check_call(["npm", "install", "-g", "@mermaid-js/mermaid-cli"])
Why it matters: Powerful and often legitimate — confirm the commands aren't built from untrusted input.
Fix: Confirm the command and its arguments are fully controlled and not derived from untrusted input; avoid shell=True.
The component reads files from disk.
const f = fs.readFileSync(file, "utf8").split("\n");Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.
Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.
The component reads files from disk.
content = path.read_text()
content = data_path.read_text()
repo._set(file_path.read_text(), file_path)
with open(result_file, "r") as file:
with open(result_path, "r") as json_file:
with open(prompt_file_path, "r", encoding="utf-8") as file:
with open(graph_file_path, "r", encoding="utf-8") as file:
with open(file_path, "r") as f:
with open(file_path, "r") as file:
task_desc = self.task_desc_path.read_text()
with open(self.record_path, "r") as record_file:
doc_content = ast.literal_eval(doc_path.read_text())
doc_content = ast.literal_eval(doc_path.read_text())
doc_content = ast.literal_eval(doc_path.read_text())
with open(os.path.join(dataset_dir, task_file), encoding="utf-8") as f:
with open(f"{state['custom_dataset_dir']}/description.md", "r", encoding="utf-8") as file:with open(dataset_info_path, "r") as file:
with open(json_dir, "r") as file:
with open(os.path.join(self.state["node_dir"], f"Node-{self.id}.pkl"), "rb") as f:with open(os.path.join(self.root_node.state["node_dir"], "node_order.json"), "r") as f:
with open(os.path.join(self.root_node.state["node_dir"], "Node-0.pkl"), "rb") as f:
with open(file_path, "r") as stream:
with open(template_path, "r", encoding="utf-8") as f:
return json.loads(result_path.read_text())
data = json.loads(result_file.read_text(encoding="utf-8"))
Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.
Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.
The component writes or deletes files on disk.
shutil.rmtree(path)
self.path.write_text(self.content, encoding="utf-8")
shutil.rmtree(path)
with open(filename, "wb") as file:
os.remove(filename)
with open("tester.txt", "a") as f:with open("tester.txt", "a") as f:with open(result_file, "w") as file:
with open(output_path, "w", encoding="utf-8") as outfile:
with open(os.path.join(directory, "graph.py"), "w", encoding="utf-8") as file:
with open(os.path.join(directory, "prompt.py"), "w", encoding="utf-8") as file:
with open(os.path.join(directory, "__init__.py"), "w", encoding="utf-8") as file:
self.record_path.write_text("")record_file = open(self.record_path, "w")
self.task_desc_path.write_text(task_desc)
with open(log_path, "a") as logfile:
with open(doc_path, "w") as outfile:
doc_path.write_text(str(doc_content))
with open(patch_file, "w", encoding="utf-8") as file:
with open(name, "w") as file:
with open(Path(self.dataset_dir, self.name, "dataset_info.json"), "w", encoding="utf-8") as file:
with open(fpath, "w") as file:
shutil.copy(node_nb_dir, copy_nb_dir)
with open(fpath, "w") as f:
os.remove(pred_path)
Why it matters: Usually legitimate, but worth confirming the paths can't be controlled by untrusted input.
Fix: Confirm which files are written/deleted and that paths cannot be influenced by untrusted input.
The component makes outbound network requests.
import requests
res = requests.post(f"{self.server}/stop")res = requests.post(
f"{self.server}/start",
json=self.reset_options,
timeout=self.request_timeout,
)res = requests.post(f"{self.server}/step", json=data, timeout=self.request_timeout)res = requests.post(f"{self.server}/pause")res = requests.post(f"{self.server}/pause")import requests
response = requests.get(url, stream=True)
import aiohttp
import requests
s = requests.Session()
requests.adapters.HTTPAdapter(max_retries=MAX_CONNECTION_RETRIES),
timeout = aiohttp.ClientTimeout(
connect=request_timeout[0],
total=request_timeout[1],
)timeout = aiohttp.ClientTimeout(total=request_timeout or TIMEOUT_SECS)
data, content_type = requests.models.RequestEncodingMixin._encode_files(files, data) # type: ignore
async with aiohttp.ClientSession() as session:
import aiohttp
import requests
import urllib
import requests
from aiohttp import ClientSession
with requests.Session() as session:
session = ClientSession()
import aiohttp
import requests
Why it matters: Usually legitimate, but confirm the destinations are expected and no sensitive data leaves.
Fix: Confirm the destination hosts are expected and that no sensitive data is sent off-host.
How attackers abuse these capabilities
Interactive labs on the attack class behind the rules above. They show the technique, not anything found in FoundationAgents/MetaGPT.
Check your own component
Run the same evidence-backed scan on any MCP server, agent skill, or package.
Scan your own componentHow we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →