Is crewai safe?
- Python shell/command execution
- Python dynamic code execution
- Unsafe deserialization
crewai is an AI python_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 7 risky constructs are reported for review. It can: dynamic code execution, filesystem read, filesystem write, mcp tools detected, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 20/100 (low).
crewai 1.15.1
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of crewai's authors. Report a false positive.
Findings (7)
It loads data with a format that can rebuild arbitrary objects (e.g. pickle, or unsafe YAML).
return pickle.load(file) # noqa: S301
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).
exec(compile(module, filename, "exec"), namespace) # nosec B102 # noqa: S102
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 component can run operating-system commands or spawn processes.
res = subprocess.run(
["/usr/sbin/system_profiler", "SPHardwareDataType"],
capture_output=True,
text=True,
timeout=2,
)res = subprocess.run(
[
"C:\\Windows\\System32\\wbem\\wmic.exe",
"csproduct",
"get",
"UUID",
], …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.
root_certs = Path(self.ca_cert_path).read_bytes()
private_key = Path(self.client_key_path).read_bytes()
certificate_chain = Path(self.client_cert_path).read_bytes()
return Path(self.private_key_path).read_text()
content = path.read_text().strip()
with open(baseline_filepath, "r") as f:
results.append(json.loads(meta_file.read_text()))
contents = source_path.expanduser().read_text(encoding="utf-8")
css_content = css_file.read_text(encoding="utf-8")
js_content = js_file.read_text(encoding="utf-8")
with open(file_path, "r", encoding="utf-8") as csvfile:
with open(path, "r", encoding="utf-8") as json_file:
with open(path, "r", encoding="utf-8") as f:
with open(config_path, encoding="utf-8") as file:
return parse_jsonc(path.read_text(encoding="utf-8"), source=path)
content = path.read_text(encoding="utf-8")
return Path(location).read_text()
with open(file_path, "rb") as f:
with open(self._path, encoding="utf-8") as read_file:
with open(self.file_path, "rb") as file:
with open(self.prompt_file, encoding="utf-8") as f:
with open(prompts_path, encoding="utf-8") as f:
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.
p.write_text(json.dumps(data, indent=2))
p.write_text(json.dumps(data, indent=2))
with open(filepath, "w") as f:
with open(baseline_filepath, "w") as f:
with open(baseline_filepath, "w") as f:
shutil.rmtree(skill_dir)
(skill_dir / _META_FILENAME).write_text(json.dumps(meta, indent=2))
shutil.rmtree(skill_dir)
css_output_path.write_text(css_content, encoding="utf-8")
js_output_path.write_text(js_content, encoding="utf-8")
output_path.write_text(html_content, encoding="utf-8")
shutil.rmtree(shard_path, ignore_errors=True)
shutil.rmtree(self._local_path, ignore_errors=True)
shutil.rmtree(self._local_path, ignore_errors=True)
shutil.rmtree(entry, ignore_errors=True)
shutil.rmtree(entry, ignore_errors=True)
with open(file_path, "w") as f:
os.remove(path)
with open(self._path, "w", encoding="utf-8") as write_file:
with open(self._path, "a", encoding="utf-8") as file:
with open(self.file_path, "wb") as f:
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 urllib.parse
import httpx
from httpx import DigestAuth
client.auth = DigestAuth(self.username, self.password)
url = httpx.URL(request.url)
auth_url = f"{self.authorization_url}?{urllib.parse.urlencode(params)}"import httpx
async with httpx.AsyncClient() as client:
from httpx import AsyncClient, Response
import httpx
async with httpx.AsyncClient(
timeout=timeout, verify=verify
) as temp_auth_client:async with httpx.AsyncClient(
timeout=timeout, headers=headers, verify=verify
) as temp_client:import httpx
async with httpx.AsyncClient(
timeout=timeout,
headers=headers,
verify=verify,
) as httpx_client:from urllib.parse import urlparse
parsed = urlparse(url)
import httpx
dl_response = httpx.get(download_url, follow_redirects=True)
from urllib.parse import urlparse
result = urlparse(url)
from httpx import (
AsyncHTTPTransport as _AsyncHTTPTransport,
HTTPTransport as _HTTPTransport,
)from httpx import Limits, Request, Response
from httpx._types import CertTypes, ProxyTypes
async_client_params["http_client"] = httpx.AsyncClient(
transport=async_transport
)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.
An MCP tool surface (manifest or tool definitions) was found.
"tools": "\nYou ONLY have access to the following tools, and should NEVER make up tools that are not listed here:\n\n{tools}\n\nIMPORTANT: Use the following format in your response:\n\n```\nThought: you should always think about what to do\ …1. @tool - decorator without arguments, uses function name
2. @tool("name") - decorator with custom name3. @tool(result_as_answer=True) - decorator with options
Only includes classes that inherit from BaseTool or functions decorated with @tool.
"Please ensure your project contains valid tools (classes inheriting from BaseTool or functions with @tool decorator)."
"or functions decorated with [bold]@tool[/bold]."
Why it matters: Just context — review which tools it offers and their permissions.
Fix: Review the declared MCP tools and their permissions.
Check your own component
Run the same evidence-backed scan on any MCP server, agent skill, or package.
Scan your own componentOr get notified if this component's risk changes:
How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →