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
- 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.
Findings (14)
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]"
import requests
session = requests.Session()
import requests
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.
It loads data with a format that can rebuild arbitrary objects (e.g. pickle, or unsafe YAML).
template = yaml.load(f) or {}return yaml.load(f)
config = yaml.load(f)
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).
!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.
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.
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 = propPattern.exec(line);
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 = triggerRegex.exec(text);
while ((match = regex.exec(line)) !== null) {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.
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.
A server is bound to all network interfaces (0.0.0.0), not just your own machine.
hostname="0.0.0.0",
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))if "0.0.0.0" in url:
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.
The component reads files from disk.
with open(param_validation_path, "r") as fin:
blob = path.read_bytes()
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(fp_mapping) as f:
with open(fp_mapping) as f:
with open(fp_mapping) as f:
with open(fp_mapping) 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.
The component writes or deletes files on disk.
with open(local_path, "wb") as f:
os.remove(local_path)
os.remove(local_path)
with open(local_path, "wb") as f:
shutil.rmtree(profile_dir, ignore_errors=True)
os.remove(file_path)
with open(file_path, "wb") as f:
with open(temp_file, "wb") as f:
os.remove(header_path)
os.remove(tmp_name)
with open(code_path, "wb") as f:
with open(runner_path, "w", encoding="utf-8") as f:
with open(args_path, "w", encoding="utf-8") as f:
shutil.rmtree(instance_dir)
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")
os.remove(temp_audio_path)
os.remove(temp_audio_path)
with open(conf_path, "w") as f:
with open("stats.txt", "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:
shutil.copyfileobj(src, 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.
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.
The component makes outbound network requests.
import http from 'node:http';
!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 …import axios from 'axios';
const ret = await axios.get(api);
const ret = await axios.get(api, {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
import axios from 'axios';
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.
The component makes outbound network requests.
import requests
session = requests.Session()
import requests
self.session = requests.Session()
import urllib.parse
encoded_key: str = urllib.parse.quote(key, safe="")
from urllib.parse import urlparse
parsed = urlparse(url)
from urllib.error import HTTPError, URLError
from urllib.parse import unquote, urlparse
from urllib.request import Request, urlopen
parsed = urlparse(token)
name = unquote(m.group(1).strip().strip('"'))parsed = urlparse(url)
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
import requests
parsed = urlparse(url)
import requests
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.
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.
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 componentOr get notified if this component's risk changes:
How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →