SkillTotal

Is infiniflow/ragflow safe?

ragflow is an AI python_package analyzed by SkillTotal's deterministic static scanner. The scan found malicious indicators (14 findings with evidence); treat it as unsafe until reviewed. It can: dynamic code execution, filesystem read, filesystem write, install time execution, mcp tools detected, network egress, prompt surface risk and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 90/100 (critical).

ragflow 0.26.3

python_package · https://github.com/infiniflow/ragflow
CRITICAL
90
/ 100 risk score
Snapshot · scanned Jul 3, 2026 · ragflow@0.26.3 · engine 0.24.0 / ruleset 25
Malicious indicators found
Why:
  • Prompt injection / instruction override
  • Sensitive-data access combined with network egress
  • Python shell/command execution

Malicious indicators detected — review findings before use.

Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of infiniflow/ragflow'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 egressprompt surface riskshell execution

Findings (14)

CRITICALSensitive-data access combined with network egressST-COMBO-EXFIL

The component both reads secret locations (keys, tokens) and can send data over the network.

#     OAuthConfig(client_id="8suvn9ik7qezsq2dub0ye6ubox61081z", client_secret="QScv…[redacted, 32 chars]")
password: 'infi…[redacted, 21 chars]'
API_KEY = "ragf…[redacted, 40 chars]"
session = requests.Session()

Why it matters: That combination is the classic path for quietly stealing credentials off your machine.

Fix: Verify that secrets read from disk are never transmitted off-host without explicit, auditable user consent.

HIGHUnsafe deserializationST-DESERIALIZE-PY

It loads data with a format that can rebuild arbitrary objects (e.g. pickle, or unsafe YAML).

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.

HIGHNode.js dynamic code executionST-DYN-NODE

The code turns strings into live code at runtime (eval / new Function / exec).

!function webpackUniversalModuleDefinition(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf.worker",[],t):"object"==typeof exports?exports["pdfjs-di …

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.

HIGHEmbedded secret / credentialST-SECRET-EMBEDDED

A hardcoded credential (API key, token, or private key) is shipped in the code.

#     OAuthConfig(client_id="8suvn9ik7qezsq2dub0ye6ubox61081z", client_secret="QScv…[redacted, 32 chars]")
password: 'infi…[redacted, 21 chars]'
API_KEY = "ragf…[redacted, 40 chars]"
rag_object = RAGFlow(api_key="ragf…[redacted, 51 chars]", base_url="http://localhost:9222")

Why it matters: Anyone who gets the package gets the secret — rotate it and load secrets at runtime instead.

Fix: Remove the secret from the code, rotate it immediately, and load credentials from the environment or a secrets manager at runtime.

HIGHNode.js shell/command executionST-SHELL-NODE

The component can run operating-system commands or spawn processes.

!function webpackUniversalModuleDefinition(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf.worker",[],t):"object"==typeof exports?exports["pdfjs-di …
const match = /language-(\w+)/.exec(className || '');
const match = /language-(\w+)/.exec(className || '');
const match = /language-(\w+)/.exec(className || '');
while ((match = currentReg.exec(text)) !== null) {
const match = /language-(\w+)/.exec(className || '');
const match = /^GMT(?<sign>\+|-)(?<hours>\d{2}):(?<minutes>\d{2})$/i.exec(
while ((match = regex.exec(content)) !== null) {
const match = PromptVariableLeadingPathRegex.exec(text);
const match = /language-(\w+)/.exec(className || '');
const match = /language-(\w+)/.exec(className || '');

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.

tar_proc = await asyncio.create_subprocess_exec("tar", "czf", "-", "-C", workdir, code_name, runner_name, str(bundle["args_name"]), stdout=asyncio.subprocess.PIPE)
docker_proc = await asyncio.create_subprocess_exec(
            "docker", "exec", "-i", container, "tar", "xzf", "-", "-C", f"/workspace/{task_id}", stdin=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
        )
proc = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
process = subprocess.Popen(
            command,
            cwd=instance_dir,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            encoding="utf-8",
            errors="replace", …
proc = await asyncio.create_subprocess_exec(
                npm,
                "install",
                "--no-fund",
                "--no-audit",
                cwd=str(gateway_dir),
            )
self._process = await asyncio.create_subprocess_exec(
            *cfg.command,
            cwd=cfg.cwd,
            env=env,
        )
proc = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=GHOSTSCRIPT_TIMEOUT_SEC,
            )
result = subprocess.run(
                psql_cmd,
                capture_output=True,
                text=True,
                timeout=10,  # 10 second timeout
            )
subprocess.check_call([sys.executable, "-m", "pip", "install", *pkg_names])
version_info = subprocess.check_output(["git", "describe", "--tags", "--match=v*", "--first-parent", "--always"]).strip().decode("utf-8")
result = subprocess.run(cmd, check=False)
proc = subprocess.run(
        ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"],
        check=True,
        capture_output=True,
        text=True,
    )
proc = subprocess.run(
        ["git", "ls-files"],
        check=True,
        capture_output=True,
        text=True,
    )

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.

MEDIUMServer bound to all network interfacesST-EXPOSE-BIND

A server is bound to all network interfaces (0.0.0.0), not just your own machine.

webhook_host: str = "0.0.0.0"
webhook_host=str(cfg.get("webhook_host", "0.0.0.0")),
webhook_host: str = "0.0.0.0"
webhook_host=str(cfg.get("webhook_host", "0.0.0.0")),
debugpy.listen(("0.0.0.0", RAGFLOW_DEBUGPY_LISTEN))
url = url.replace("0.0.0.0", "127.0.0.1")
uvicorn.run(app, host="0.0.0.0", port=8000)

Why it matters: Without authentication, other hosts on the network can reach it.

Fix: Bind to 127.0.0.1 for local-only use, or require authentication and restrict access if remote exposure is intended.

MEDIUMPython filesystem readST-FS-PY-READ

The component reads files from disk.

with open(param_validation_path, "r") as fin:
with open(file_path, "rb") as f:
with open(file_path, "rb") as f:
with open(file_path, "rb") as f:
with open(file_path, "rb") as f:
with open(tmp_name, "rb") as f:
"content_b64": base64.b64encode(path.read_bytes()).decode("ascii"),
with open(template_path, "r", encoding="utf-8") as f:
with open(os.path.join(get_project_base_directory(), "conf", "system_settings.json"), "r") as f:
with open(template_path, "r", encoding="utf-8") as f:
rsa_key = RSA.importKey(Path(file_path).read_text(), "Welcome")
rsa_key = RSA.importKey(Path(file_path).read_text(), "Welcome")
pem = Path(file_path).read_text()
with open(conf_path) as f:
with open(fp_mapping, "r") as f:
with open(fp_mapping, "r") as f:
with open(os.path.join(get_project_base_directory(), "conf", "llm_factories.json"), "r") as f:
with open(version_path, "r") as f:
with open(src_path, "rb") 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.

MEDIUMPython filesystem write/deleteST-FS-PY-WRITE

The component writes or deletes files on disk.

with open(local_path, "wb") as f:
with open(local_path, "wb") as f:
shutil.rmtree(profile_dir, ignore_errors=True)
with open(file_path, "wb") as f:
with open(temp_file, "wb") as f:
with open(runner_path, "w", encoding="utf-8") as f:
with open(args_path, "w", encoding="utf-8") as f:
script_path.write_text(build_python_wrapper(code, args_json), encoding="utf-8")
script_path.write_text(build_javascript_wrapper(code, args_json), encoding="utf-8")
with open(conf_path, "w") as f:
shutil.copyfile(bundled_encoding_path, cached_encoding_path)
with open(tmp_pdf, "wb") as f:
with zip_ref.open(member) as src, open(dest_path, "wb") as dst:
with open(output_zip_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.

MEDIUMnpm prepare hookST-INSTALL-NPM-PREPARE

package.json has a 'prepare' script (runs on git/local installs and before publishing).

"prepare": "cd .. && git rev-parse --git-dir >/dev/null 2>&1 && lefthook install || true",

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.

!function webpackUniversalModuleDefinition(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf.worker",[],t):"object"==typeof exports?exports["pdfjs-di …
const { data } = await axios.get(url, { headers: httpHeaders });
item.promise = fetch(url, { headers: { [Authorization]: authorization } })
const response = await fetch(api.chatsTranscriptions, {
[ProgrammingLanguage.Javascript]: `const axios = require('axios');
const response = await axios.get('https://github.com/infiniflow/ragflow');
import axios from 'axios';
const ret = await axios.get('/conf.json');
const response = await fetch(url, {
const response = await fetch(url, {
const response = await fetch(url, {
import { type AxiosResponseHeaders } from 'axios';
const response = await fetch(url, { headers });
const response = await fetch(downloadUrl);
const response = await fetch('/api/v1/skills/search', {
const indexResponse = await fetch('/api/v1/skills/index', {
const response = await fetch('/api/v1/skills/status', {
* - the API fetch (with edit-mode pre-check seeding) and modal-reset effect
const request = axios.create({
import axios from 'axios';

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.

session = requests.Session()
self.session = requests.Session()
encoded_key: str = urllib.parse.quote(key, safe="")
from urllib.parse import urlparse
from urllib.error import HTTPError, URLError
from urllib.parse import unquote, urlparse
from urllib.request import Request, urlopen
name = unquote(m.group(1).strip().strip('"'))
name = unquote(raw_name).strip()
req = Request(url, headers={"User-Agent": "RAGFlow-Browser-Node/1.0"})
with urlopen(req, timeout=30) as response:
from urllib.parse import urlparse
response = requests.post(url, json=payload, timeout=exec_timeout, headers={"Content-Type": "application/json"})
response = requests.get(url, timeout=5)

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.

MEDIUMPrompt injection / instruction overrideST-PROMPT-INJECTION

Text that tries to override an AI's instructions was found (e.g. 'ignore previous instructions').

Description: "prompt injection: ignore previous instructions",
Description: "DAN (Do Anything Now) jailbreak attempt",
- Prompt injection attempts (DAN mode, ignore instructions, etc.)

Why it matters: Embedded in a component, it can hijack an agent's behavior or hide actions from you.

Fix: Treat embedded instructions as untrusted. Review whether this component attempts to manipulate an agent's behavior or hide actions from users.

LOWMCP tool surface detectedST-MCP-DETECTED

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

"""Lightweight ``@tool`` decorator and matching ``ToolCallSession`` adapter.
``@tool`` callable apart from a raw schema dict.
"""Adapter that lets a list of ``@tool``-decorated callables satisfy the
raise TypeError(f"{getattr(fn, '__name__', fn)!r} is not a @tool-decorated callable")

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

Or get notified if this component's risk changes:

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