Back to skill

Security audit

ZeeLin Deep Research 深度研究

Security checks for vulnerabilities and agentic risk

Overview

The skill matches its research purpose, but it needs Review because it sends research content to an external service, auto-messages report links, and handles credentials and temporary logs with weak safeguards.

Install only if you are comfortable sending research prompts and topics to ZeeLin, storing a ZeeLin API key in a local config file, and allowing the skill to automatically send PDF report links through OpenClaw messaging. Use a dedicated low-privilege API key, restrict config.json permissions, avoid sensitive prompts unless approved for that provider, and review or disable automatic notification behavior if recipient routing matters.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/zeelin_start.py:21
Finding

Sensitive configuration is persisted without restrictive file permissions

Content
View full analysis

Vulnerability Details

File Location: scripts/zeelin_start.py:21-49
Vulnerability Type: Plaintext sensitive-data storage with insufficient access-control enforcement
Risk Level: Medium

The Skill stores its ZeeLin API key in config.json and adds messaging recipient information obtained from ZEELIN_TARGET_USER and ZEELIN_CHANNEL. The code does not enforce restrictive permissions when writing this file.

Relevant code:

python
def save_config(config):
    """保存配置到文件"""
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f, ensure_ascii=False, indent=2)

def ensure_target_config():
    """确保配置中有 target_user 和 channel,如果没有则尝试从环境变量获取并保存"""
    config = load_config()
    
    # 检查是否需要更新
    needs_save = False
    
    # 尝试从环境变量获取
    target_user = os.environ.get("ZEELIN_TARGET_USER", "")
    channel = os.environ.get("ZEELIN_CHANNEL", "")
    
    if target_user and not config.get("target_user"):
        config["target_user"] = target_user
        needs_save = True
        print(f"自动配置 target_user: {target_user}")
    
    if channel and not config.get("channel"):
        config["channel"] = channel
        needs_save = True
        print(f"自动配置 channel: {channel}")
    
    if needs_save:
        save_config(config)
        print(f"配置已保存到 {CONFIG_FILE}")

The documented configuration also places the API credential in the same file:

json
{
  "api_key": "your API key"
}

Technical Analysis

Python's ordinary open(path, "w") call does not guarantee owner-only permissions. For a newly created file, its effective permissions depend on the process umask. For an existing file, the existing mode is retained. The implementation neither creates the file with mode 0600 nor checks whether an existing configuration file is accessible by group or other users.

Consequently, the API key, target user identifier, and messaging channel can ...[truncated 1626 chars]

Remediation
View remediation

Remediation Suggestions

  1. Store the API key in the platform's secret-management facility or retrieve it from a dedicated environment variable instead of config.json.
  2. Avoid persisting target_user and channel unless persistence is operationally required.
  3. If file-based storage is unavoidable, create the file atomically with owner-only mode:
    python
    fd = os.open(CONFIG_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        json.dump(config, f, ensure_ascii=False, indent=2)
    
  4. Check existing file permissions with stat before loading secrets and reject or repair files accessible by group or other users.
  5. Use atomic replacement through a securely created temporary file in the same directory to prevent partial writes and race conditions.
  6. Remove recipient identifiers from informational log output.
  7. Ensure config.json is excluded from source control, published packages, backups not designed for secrets, and broadly accessible diagnostic bundles.
  8. Document that the API key and recipient metadata are sensitive and provide a credential-rotation procedure.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/zeelin_start.py:88
Finding

Server-controlled session identifier is used in an unsafe predictable temporary-file path

Content
View full analysis

Vulnerability Details

File Location: scripts/zeelin_start.py:88-118
Vulnerability Type: Unsafe temporary-file construction and potential path traversal or symbolic-link overwrite
Risk Level: Medium

The session identifier returned by the remote ZeeLin API is interpolated directly into a predictable pathname under the shared /tmp directory. The resulting file is then opened with truncation enabled.

Relevant code:

python
session_id = data.get("data", {}).get("sessionId")
title = data.get("data", {}).get("title", CONTENT)

if not session_id:
    print(f"ERROR: Failed to create task: {data}")
    sys.exit(1)

print(f"CREATED: session_id={session_id}")
print(f"TITLE: {title}")

# 启动子代理监控
# 方式1: 后台运行(环境变量问题,可能失效)
# 方式2: 直接运行(会阻塞,但能在子代理中正确发送消息)

# 检查是否是交互式调用
log_file = f"/tmp/zeelin_watch_{session_id}.log"

if sys.stdout.isatty():
    # 交互式,后台运行
    env = os.environ.copy()
    env["ZEELIN_SESSION_ID"] = session_id
    env["ZEELIN_CONTENT"] = title
    
    import subprocess
    with open(log_file, "w") as f:
        subprocess.Popen(
            ["nohup", "python3", "-u", str(WATCH_SCRIPT)],
            env=env,
            stdout=f,
            stderr=subprocess.STDOUT
        )

Technical Analysis

session_id crosses a network trust boundary because it originates in the JSON response from https://desearch.zeelin.cn. The code only verifies that the value is nonempty; it does not enforce a restricted character set, maximum length, or expected type.

If a malicious or compromised server returns path separators or traversal components, interpolation can cause the path to resolve outside the intended /tmp location. For example, a session identifier containing directory traversal components could target another path writable by the current user.

Even when session identifiers contain only expected characters, the pathname is predictable and located in a share ...[truncated 1896 chars]

Remediation
View remediation

Remediation Suggestions

  1. Validate session_id immediately after parsing the response. Require a string that matches a strict allowlist and length limit, for example:
    python
    import re
    
    if not isinstance(session_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", session_id):
        raise ValueError("Invalid session identifier")
    
  2. Do not derive temporary filenames directly from remote input.
  3. Create a private temporary directory with mode 0700, then use tempfile.mkstemp() or tempfile.NamedTemporaryFile():
    python
    import tempfile
    
    log_dir = tempfile.mkdtemp(prefix="zeelin-watch-")
    os.chmod(log_dir, 0o700)
    fd, log_file = tempfile.mkstemp(prefix="watch-", suffix=".log", dir=log_dir)
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        subprocess.Popen(
            ["python3", "-u", str(WATCH_SCRIPT)],
            env=env,
            stdout=f,
            stderr=subprocess.STDOUT,
        )
    
  4. If a deterministic file is required, use exclusive, no-follow creation where supported and reject any pre-existing path or symbolic link.
  5. Avoid invoking nohup as an argument to subprocess.Popen unless it is operationally necessary; Python can start the watcher directly with an appropriate detached-session configuration.
  6. Define log retention and secure deletion behavior so task metadata is not left indefinitely in a shared temporary location.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (22)

Tp2

High
Category
MCP Tool Poisoning
Confidence
85% confidence
Finding

Mixing characters from multiple Unicode scripts in a single identifier is a common technique to create visually ambiguous tool names.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The declared purpose emphasizes research generation, but the documented behavior also includes background task orchestration, status polling, automatic user identification, and outbound delivery of PDF links. This mismatch is dangerous because users may authorize a research tool without realizing it also performs asynchronous monitoring and message delivery through external channels.

Content

No source excerpt is available for this finding.

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/zeelin_start.py (reported line 107)May include surrounding context.

python
if sys.stdout.isatty():
    # 交互式,后台运行
    env = os.environ.copy()
    env["ZEELIN_SESSION_ID"] = session_id
    env["ZEELIN_CONTENT"] = title

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/zeelin_start.py (reported line 123)May include surrounding context.

python
if sys.stdout.isatty():
    # 交互式,后台运行
    env = os.environ.copy()
    env["ZEELIN_SESSION_ID"] = session_id
    env["ZEELIN_CONTENT"] = title

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill declares executable capabilities including shell, network, file read/write, and environment access, but provides no explicit tool scope or permission boundaries. In an agent environment, this creates excessive implicit trust and increases the chance that the skill can access local files, credentials, or external services beyond what users reasonably expect.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

Automatically obtaining the user's channel and ID without a privacy notice or consent mechanism creates an undisclosed collection of identifiers. Even if used for benign routing, such identifiers can enable tracking, correlation across sessions, or unintended disclosure through third-party services.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
91% confidence
Finding

This duplicate external-transmission finding points to the same outbound request that sends prompt content to a third-party service. The behavior matches the product purpose, but it remains a genuine privacy/security concern because data leaves the host without visible safeguards in this file.

Content

Scanner excerpt · scripts/zeelin_start.py (reported line 71)May include surrounding context.

python
ensure_target_config()

# 创建任务
resp = requests.post(
    "https://desearch.zeelin.cn/api/conversation/anew",
    json={
        "content": CONTENT,

External Transmission

Medium
Category
Data Exfiltration
Confidence
91% confidence
Finding

This duplicate external-transmission finding points to the same outbound request that sends prompt content to a third-party service. The behavior matches the product purpose, but it remains a genuine privacy/security concern because data leaves the host without visible safeguards in this file.

Content

Scanner excerpt · scripts/zeelin_start.py (reported line 71)May include surrounding context.

python
ensure_target_config()

# 创建任务
resp = requests.post(
    "https://desearch.zeelin.cn/api/conversation/anew",
    json={
        "content": CONTENT,

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

User-supplied CONTENT is transmitted to an external service along with an API key, but this file provides no user-facing disclosure, confirmation, or redaction step. In a research-assistant skill, prompts may contain sensitive business or personal data, so silent transmission to a third party is a meaningful privacy and data-governance risk.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

The script conditionally launches a background monitor and writes logs under /tmp without a clear warning before doing so. Hidden background activity and log creation are risky in agent environments because they can expose task metadata to other local users and make behavior less transparent to operators.

Content

No source excerpt is available for this finding.

Tainted flow: 'log_file' from requests.post (line 103, network input) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/zeelin_start.py (reported line 112)May include surrounding context.

python
env["ZEELIN_CONTENT"] = title
    
    import subprocess
    with open(log_file, "w") as f:
        subprocess.Popen(
            ["nohup", "python3", "-u", str(WATCH_SCRIPT)],
            env=env,

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/zeelin_start.py (reported line 113)May include surrounding context.

python
import subprocess
    with open(log_file, "w") as f:
        subprocess.Popen(
            ["nohup", "python3", "-u", str(WATCH_SCRIPT)],
            env=env,
            stdout=f,

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The skill starts a separate local watcher process, including unattended background execution in interactive mode, which increases the skill's local execution surface beyond simply submitting a research request. In an agent/skill context, spawning persistent helper processes can surprise operators, consume resources, and create opportunities for hidden follow-on behavior in the companion script.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/zeelin_start.py (reported line 128)May include surrounding context.

python
env["ZEELIN_CONTENT"] = title
    
    import subprocess
    subprocess.run(
        ["python3", "-u", str(WATCH_SCRIPT)],
        env=env
    )

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

This Python file contains natural-language docstrings, comments, and user-visible messages entirely in Chinese, including status/error output and the final notification content. Because the skill does not offer any user language choice or explain that it is intentionally limited to a Chinese-speaking context, it appears to enforce a specific language without user opt-in.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

This monitor script directly messages a user through an external CLI when a task completes, creating an outbound communication channel from automated research activity to an end user. In agent environments, that can bypass normal review or approval flows and could be abused to send unvetted links or content, especially because the PDF URL and title come from remote/runtime-controlled sources.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/zeelin_watch.py (reported line 68)May include surrounding context.

python
"--target", TARGET_USER,
        "--message", f"✅ 调研完成:{title}\n\n📥 PDF下载链接:{pdf_url}"
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    print(f"NOTIFY_OUTPUT: {result.stdout}")
    if result.returncode != 0:
        print(f"NOTIFY_ERROR: {result.stderr}")

Tainted flow: 'cmd' from os.environ.get (line 62, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/zeelin_watch.py (reported line 68)May include surrounding context.

python
"--target", TARGET_USER,
        "--message", f"✅ 调研完成:{title}\n\n📥 PDF下载链接:{pdf_url}"
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    print(f"NOTIFY_OUTPUT: {result.stdout}")
    if result.returncode != 0:
        print(f"NOTIFY_ERROR: {result.stderr}")

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

Automatically sending a PDF download link to the user is an outbound delivery action that may expose report contents, metadata, or access tokens through external channels without clear consent. The risk is elevated because the skill is designed for research tasks, which may involve sensitive business or market-analysis content.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
77% confidence
Finding

The module docstring presents the skill description exclusively in Chinese, with no indication that users can choose another language or that the locale is intentionally constrained. This can violate a language/locale policy when no opt-in or justification is provided.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The script reads ZEELIN_TARGET_USER and ZEELIN_CHANNEL from the environment and persists them into config.json without clear necessity for a local research launcher. Persisting operator environment-derived values creates unnecessary local data retention and can expose contextual identifiers to later processes or users on the same system.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The code consumes ZEELIN_SESSION_ID and ZEELIN_CONTENT from process environment to drive monitoring and notification behavior. For a skill presented primarily as a research platform, dependence on external runtime environment state is an extra operational capability not reflected in the manifest's user-facing purpose.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.