Is firecrawl/firecrawl safe?
- Python shell/command execution
- Node.js shell/command execution
- npm install-time lifecycle hook
repo is an AI ai_component analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 12 risky constructs are reported for review. It can: dynamic code execution, filesystem read, filesystem write, install time execution, mcp tools detected, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 0/100 (low).
repo
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of firecrawl/firecrawl'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 (12)
The code turns strings into live code at runtime (eval / new Function / exec).
const result = await getRedisConnection().eval(
const result = (await redisRateLimitClient.eval(
const total = (await redisRateLimitClient.eval(
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.
package.json runs scripts automatically when the package is installed.
"install": "pnpm build"
Why it matters: Install scripts are a favorite supply-chain foothold — they execute on every machine that installs the package.
Fix: Inspect the hook command. Install-time scripts are a common supply chain execution vector; ensure they do nothing beyond a documented build step.
The component can run operating-system commands or spawn processes.
import { spawnSync } from "node:child_process";return spawnSync(command, args, {import { type ChildProcess, spawn } from "child_process";child = spawn("cmd", ["/c", command], {child = spawn("sh", ["-c", command], {child = spawn(cmd, args, {const killer = spawn(
await pipeline.exec();
await pipeline.exec();
await pipeline.exec();
await pipeline.exec();
await pipeline.exec();
await pipeline.exec();
const results = await pipeline.exec();
await uniquePipeline.exec();
const results = await pipeline.exec();
while ((match = SENTENCE_END.exec(window)) !== null) {await pipeline.exec();
const results = await pipeline.exec();
const results = await pipeline.exec();
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; prefer execFile with an argument array.
The component can run operating-system commands or spawn processes.
return subprocess.check_output(["git", *args], text=True).strip()
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 load = JSON.parse(await fs.readFile(mockPath, "utf8"));
const data = fs.readFileSync("/sys/fs/cgroup/memory.current", "utf8");const data = fs.readFileSync("/sys/fs/cgroup/memory.max", "utf8").trim();const data = fs.readFileSync("/sys/fs/cgroup/cpu.stat", "utf8");const data = fs.readFileSync(cpusetPath, "utf8").trim();
// const logs = fs.readFileSync("7a373219-0eb4-4e47-b2df-e90e12afd5c1.log", "utf8")].flatMap(x => JSON.parse(fs.readFileSync(x, "utf8"))).map(x => x.jsonPayload);
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.
await fs.appendFile(this.filePath, this.lines.join("\n") + "\n");await fs.unlink(tempFilePath);
await fs.writeFile(
// await fs.writeFile(
fs.writeFileSync("crawl-" + crawlId + ".log", crawlLogs.map(x => JSON.stringify(x)).join("\n"));fs.writeFileSync(crawlId + ".md",
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 reads files from disk.
version_file = Path(file_path).read_text()
with open(file_path, 'r') as file:
build_file = Path(file_path).read_text()
version_file = Path(file_path).read_text()
csproj_file = Path(file_path).read_text()
version_file = Path(file_path).read_text()
version_file = Path(os.path.join(package_path, '__init__.py')).read_text()
version_file = Path(os.path.join(package_path, '__init__.py')).read_text()
file_bytes = file_path.read_bytes()
version_file = (package_path / "__init__.py").read_text()
long_description_content = (this_directory / "README.md").read_text()
version_file = (this_directory / "firecrawl" / "__init__.py").read_text()
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.
with open(github_output, "a", encoding="utf-8") as output:
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.
package.json has a 'prepare' script (runs on git/local installs and before publishing).
"prepare": "cd ../.. && husky ./apps/api/.husky",
Why it matters: Usually a build step, but confirm it doesn't fetch or run remote code.
Fix: Usually a legitimate build step; confirm it only builds and does not fetch or execute remote code.
The component makes outbound network requests.
const response = await fetch(url, {const response = await fetch(`${config.FIRE_ENGINE_BETA_URL}/scrape`, {const resp = await fetch(
const upstream = await fetch(
const optionsRequest = await fetch(
const upstream = await fetch(
const passthrough = await fetch(
const res = await fetch(url, {const upstream = await fetch(target, {import http from "node:http";
import https from "node:https";
upstream = await fetch(url, {const res = await fetch(`${config.AVGRAB_SERVICE_URL}/supported-urls`);const response = await fetch(`${config.AVGRAB_SERVICE_URL}/resolve`, {const response = await fetch(`${baseUrl}${EXCHANGE_PROVIDERS_PATH}`, {const response = await fetch(
response = await fetch(`${config.FIRE_PRIVACY_URL}/redact`, {import axios, { AxiosInstance, AxiosError } from "axios";* Convert HTML to Markdown using direct axios call
const response = await axios.post<ConvertResponse>(
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.
The component makes outbound network requests.
import requests
response = requests.get(f"https://pypi.org/pypi/{package_name}/json")response = requests.get(f"https://registry.npmjs.org/{package_name}/latest")response = requests.get(f"https://rubygems.org/api/v1/versions/{package_name}/latest.json")response = requests.get(url)
response = requests.get(url)
response = requests.get(url)
response = requests.get(
f"https://crates.io/api/v1/crates/{package_name}",
headers={"User-Agent": "firecrawl-version-check"}
)import requests
response = requests.post(
f"{args.api_url}/run",
json={
"experiment_id": args.experiment_id,
"api_key": args.api_key,
"label": args.label
},
hea …import requests
import aiohttp
response = requests.post(
f'{self.api_url}/v1/scrape',
headers=_headers,
json=scrape_params,
timeout=(timeout / 1000.0 + 5 if timeout is not None else None)
)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.
const server = new Server({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 componentHow we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →