SkillTotal

Is firecrawl/firecrawl safe?

No malicious indicators - review capabilities before installing
Notable — review in context (capabilities are not malware):
  • 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

ai_component · https://github.com/firecrawl/firecrawl
LOW
0
/ 100 risk score
Snapshot · scanned Aug 16, 2026 · repo@c54c3f7 · engine 0.38.1 / ruleset 42

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.

Capabilities — what this component can do (not a risk score):
dynamic code executionfilesystem readfilesystem writeinstall time executionmcp tools detectednetwork egressshell execution

Behavioral traits

How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.

Tool surface
Tool Usage
Execution authority
Tool Access Control / Direct Tool Access
Filesystem reach
Tool Execution Context
Network egress
Interaction & Communication / Direct Communication
Supply-chain provenance risk
General Protections / Supply Chain

Findings (12)

HIGHNode.js dynamic code executionST-DYN-NODE

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.

HIGHnpm install-time lifecycle hookST-INSTALL-NPM

package.json runs scripts automatically when the package is installed.

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.

HIGHNode.js shell/command executionST-SHELL-NODE

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 results = await pipeline.exec();
const results = await pipeline.exec();
while ((match = SENTENCE_END.exec(window)) !== null) {
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.

HIGHPython shell/command executionST-SHELL-PY

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.

MEDIUMNode.js filesystem readST-FS-NODE-READ

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.

MEDIUMNode.js filesystem write/deleteST-FS-NODE-WRITE

The component writes or deletes files on disk.

await fs.appendFile(this.filePath, this.lines.join("\n") + "\n");
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.

MEDIUMPython filesystem readST-FS-PY-READ

The component reads files from disk.

version_file = Path(file_path).read_text()
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()
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.

MEDIUMPython filesystem write/deleteST-FS-PY-WRITE

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.

MEDIUMnpm prepare hookST-INSTALL-NPM-PREPARE

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.

MEDIUMNode.js network egressST-NET-NODE

The component makes outbound network requests.

const response = await fetch(url, {
const response = await fetch(`${config.FIRE_ENGINE_BETA_URL}/scrape`, {
const passthrough = await fetch(
const upstream = await fetch(target, {
import http from "node:http";
import https from "node:https";
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.

MEDIUMPython network egressST-NET-PY

The component makes outbound network 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(
        f"https://crates.io/api/v1/crates/{package_name}",
        headers={"User-Agent": "firecrawl-version-check"}
    )
response = requests.post(
            f"{args.api_url}/run",
            json={
                "experiment_id": args.experiment_id,
                "api_key": args.api_key,
                "label": args.label
            },
            hea …
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.

LOWMCP tool surface detectedST-MCP-DETECTED

An MCP tool surface (manifest or tool definitions) was found.

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 component

How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →