Back to skill

Security audit

Obsidian Inbox Pipeline

Security checks for vulnerabilities and agentic risk

Overview

This Obsidian automation skill is not clearly malicious, but it needs review because it can run other local scripts, write/read vault files, and send content externally with weak scoping controls.

Review before installing, especially if you plan to run it from cron. Use a dedicated low-privilege account, restrict OBSIDIAN_VAULT_PATH and OBSIDIAN_INBOX_DIR to the intended vault, avoid untrusted radar_type values, secure or disable Telegram/Feishu credentials unless you want external sending, and replace predictable /tmp files with private temporary directories.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/daily_pipeline.sh:25
Finding
Unvalidated radar type permits path traversal and execution of unintended Python scripts<![CDATA[ ## Vulnerability Details **File Location**: `scripts/daily_pipeline.sh`, lines 25–56 **Vulnerability Type**: Path traversal, unsafe executable path construction, and Python source injection **Risk Level**: High ### Vulnerable Code ```bash RADAR_TYPE="${1:?用法: $0 <radar_type> <name> <category> <emoji> <source>}" RADAR_NAME="${2:?}" CATEGORY="${3:?}" EMOJI="${4:?}" SOURCE="${5:?}" TODAY="$(date '+%Y-%m-%d')" TMP="/tmp/radar_${RADAR_TYPE}.md" # ── Step 1:生成日报 ────────────────────────────────── RADAR_MAIN="$SKILL_DIR/../${RADAR_TYPE}/main.py" if [[ -f "$RADAR_MAIN" ]]; then python3 "$RADAR_MAIN" > "$TMP" else echo "⚠️ 未找到 $RADAR_MAIN,跳过生成步骤" exit 0 fi # ── Step 2:写入 Obsidian inbox ────────────────────── # 模板:用 capture.py 写入(结构化版本) # 如 radar 提供了独立格式脚本,优先用其写入结构化 Obsidian 文件 FORMAT_SCRIPT="$SKILL_DIR/../${RADAR_TYPE}/format_push.py" if [[ -f "$FORMAT_SCRIPT" ]]; then python3 "$FORMAT_SCRIPT" \ "$RADAR_TYPE" "$RADAR_NAME" "$SOURCE" "$CATEGORY" "$EMOJI" "$TODAY" \ > /dev/null 2>&1 OBSIDIAN_PATH="$(python3 -c " import sys for line in open('$TMP'): if line.startswith('OBSIDIAN|'): print(line.split('|',1)[1].strip()) ")" ``` ### Technical Analysis `RADAR_TYPE` is taken directly from the first command-line argument without an allowlist, canonicalization, or containment check. It is then used to construct the paths of `main.py` and `format_push.py`, both of which are executed by the pipeline. Because values such as `../` are not rejected, the resolved script path can escape the expected sibling skill directory. Shell quoting prevents ordinary shell metacharacter expansion at these execution sites, but it does not prevent filesystem traversal. The same value also controls `TMP`, which is embedded directly inside Python source passed to `python3 -c`. A value containing a single quote and valid Python syntax could modify the generated Python program. Reaching that code requires the earlier generated paths to satisfy the file che ...[truncated 1502 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict `RADAR_TYPE` to a narrow identifier format before using it: ```bash if [[ ! "$RADAR_TYPE" =~ ^[A-Za-z0-9_-]+$ ]]; then echo "Invalid radar type" >&2 exit 1 fi ``` 2. Define a trusted skill root and canonicalize every executable path with `realpath`. 3. Verify that each resolved path is a descendant of the trusted skill root before execution. 4. Consider using an explicit allowlist mapping radar identifiers to approved script paths rather than constructing executable paths from user input. 5. Never interpolate shell-controlled values into Python source. Pass the temporary path as an argument: ```bash python3 - "$TMP" <<'PY' import sys with open(sys.argv[1], encoding="utf-8") as file: for line in file: if line.startswith("OBSIDIAN|"): print(line.split("|", 1)[1].strip()) PY ``` 6. Run scheduled pipelines under a dedicated least-privileged account with access only to the required skill directories and vault paths. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/daily_pipeline.sh:31
Finding
Predictable shared temporary files enable symlink overwrite and stale-content attacks<![CDATA[ ## Vulnerability Details **File Location**: `scripts/daily_pipeline.sh`, lines 31–36, 73–79, 106–116, and 152 **Vulnerability Type**: Insecure temporary-file handling **Risk Level**: High ### Vulnerable Code ```bash TODAY="$(date '+%Y-%m-%d')" TMP="/tmp/radar_${RADAR_TYPE}.md" # ── Step 1:生成日报 ────────────────────────────────── RADAR_MAIN="$SKILL_DIR/../${RADAR_TYPE}/main.py" if [[ -f "$RADAR_MAIN" ]]; then python3 "$RADAR_MAIN" > "$TMP" ``` ```bash if [[ -n "${TELEGRAM_BOT_TOKEN:-}" && -n "${TELEGRAM_CHAT_ID:-}" ]]; then PUSH_FILE="/tmp/radar_push_${RADAR_TYPE}.txt" python3 - "$PUSH_FILE" << 'PYEOF' import sys, json, urllib.request, urllib.parse push_file = sys.argv[1] with open(push_file) as f: text = f.read() ``` ```bash # read push file push_file = f"/tmp/radar_push_{radar_type}.txt" with open(push_file) as f: content = f.read() ``` ```bash echo "✅ $RADAR_NAME 流水线完成" rm -f "$TMP" ``` ### Technical Analysis The pipeline uses deterministic names in the globally shared `/tmp` directory. It does not create a private temporary directory, atomically create files, verify ownership, reject symbolic links, or apply restrictive permissions. The shell redirection `> "$TMP"` follows symbolic links. A local attacker who can write to `/tmp` can pre-create the predictable path as a symlink to another file writable by the pipeline account. When the scheduled pipeline runs, Python output is redirected through that symlink and truncates or overwrites the target. The notification file is also predictable and is opened without validating its type, owner, or origin. The fallback capture branch does not create `PUSH_FILE`, so a stale or attacker-created file can be consumed. Temporary files may also contain report content and are not protected with a restrictive `umask`. The current notification blocks contain a separate functional defect: both reference `os.environ` without importing `os`. Consequently, the inspected version fails before succ ...[truncated 1480 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Set a restrictive file-creation mask: ```bash umask 077 ``` 2. Create a private temporary directory atomically: ```bash TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/obsidian-pipeline.XXXXXXXX")" trap 'rm -rf -- "$TMP_DIR"' EXIT HUP INT TERM TMP="$TMP_DIR/radar.md" PUSH_FILE="$TMP_DIR/push.txt" ``` 3. Pass the generated push-file path explicitly to every producer and consumer rather than independently reconstructing it. 4. Ensure every execution branch creates the push file before notification logic runs. 5. Refuse to process files that are symbolic links or not owned by the pipeline account. 6. Avoid placing sensitive report content in a shared directory. 7. Add `import os` to the Python notification blocks, but only after securing the temporary-file workflow. 8. Check API responses and file-operation failures, and terminate safely without leaving temporary artifacts. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
capture.py:113
Finding
Unvalidated inbox directory can escape the configured Obsidian vault<![CDATA[ ## Vulnerability Details **File Locations**: - `capture.py`, lines 113–123 - `review.py`, lines 151–153 **Vulnerability Type**: Directory traversal and vault-boundary violation **Risk Level**: Medium ### Vulnerable Code From `capture.py`: ```python vault = get_vault() inbox_dir = os.environ.get("OBSIDIAN_INBOX_DIR", "inbox").strip().lstrip("/") inbox_path = os.path.join(vault, inbox_dir) os.makedirs(inbox_path, exist_ok=True) content = build_capture_md(args) filename = f"{DATE}-{slugify(args.title)}.md" filepath = os.path.join(inbox_path, filename) if args.dry_run: print(f"[DRY RUN] 文件: {filename}") print(content) return write_note(filepath, content) ``` From `review.py`: ```python vault = get_vault() inbox_dir = os.environ.get("OBSIDIAN_INBOX_DIR", "inbox").strip().lstrip("/") items = read_inbox_items(vault, inbox_dir) ``` The review function subsequently joins and reads that path: ```python def read_inbox_items(vault: str, inbox_dir: str) -> list: inbox_path = os.path.join(vault, inbox_dir) pattern = os.path.join(inbox_path, "*.md") files = glob.glob(pattern) items = [] for path in sorted(files, key=os.path.getmtime, reverse=True): try: with open(path, encoding='utf-8') as f: content = f.read() ``` ### Technical Analysis Calling `lstrip("/")` only removes leading slash characters. It does not remove or reject parent-directory components such as `..`, nor does it prevent escape through symbolic links. For example, an `OBSIDIAN_INBOX_DIR` value of `../../target` causes `os.path.join(vault, inbox_dir)` to produce a path that resolves outside the configured vault. No canonical path comparison is performed before directories are created, notes are written, or Markdown files are read. This violates the Skill's declared vault-oriented access boundary. Although controlling an environment variable usually requires configuration-level access, environment values may be inherited ...[truncated 1424 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject absolute paths and any path containing a `..` component. 2. Canonicalize both the vault and candidate inbox directories before use. 3. Verify containment with `pathlib.Path.relative_to()` or `os.path.commonpath()`: ```python from pathlib import Path vault_path = Path(get_vault()).expanduser().resolve() configured = os.environ.get("OBSIDIAN_INBOX_DIR", "inbox").strip() relative = Path(configured) if relative.is_absolute() or ".." in relative.parts: raise ValueError("OBSIDIAN_INBOX_DIR must be a safe vault-relative path") inbox_path = (vault_path / relative).resolve() try: inbox_path.relative_to(vault_path) except ValueError: raise ValueError("Inbox directory escapes the configured vault") ``` 4. Perform the same validation in both `capture.py` and `review.py`, preferably through one shared configuration function. 5. Account for symlink escapes by validating the resolved destination after parent directories exist and immediately before reading or writing. 6. Open newly created notes using an exclusive or explicitly chosen overwrite policy to avoid silently replacing an existing note with the same date and slug. 7. Run the Skill under an account whose filesystem permissions are limited to the intended vault. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • 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
Findings (26)

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding
该代码块的核心功能是把命令行提供的内容格式化为 Markdown 并写入 Obsidian inbox。它不包含从任意来源自动抓取内容的逻辑,也没有 Telegram、飞书或定时任务相关实现。虽然“结构化写入 Obsidian inbox”这一点与声明部分一致,但声明强调的是完整的自动采集与推送流水线,而代码实际只覆盖了其中末端的写入步骤,因此描述明显高于代码实际能力,构成能力层面的不匹配。

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
描述强调的是“自动采集+入库+推送+定时”的完整自动化流水线,而该代码块并不执行采集、转换、写入、推送或调度。它只读取本地 Obsidian Vault 的索引文件和可选正文搜索结果,生成查询报告,主用途是检索知识库而非沉淀知识内容。虽然同属 Obsidian 场景,但核心功能与声明明显不符,且代码具备一个描述中未提到的主要能力:搜索知识库。

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
声明描述的是“采集+入库+推送+定时”的完整知识沉淀流水线,但该代码片段的实际作用是一个 review 工具:扫描 Obsidian inbox 下已有 .md 文件,提取元数据与摘要,按类型汇总,输出整理建议,并可写入 review 报告文件。它不连接任何外部数据源,不抓取内容,不执行推送,也没有定时触发逻辑。因此其主要用途与声明存在实质性不一致。虽然两者都与 Obsidian/inbox 相关,但代码行为明显偏向‘整理回顾’而非‘自动采集写入流水线’。

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
整体上,这段代码的核心目的与声明大体接近:它确实是在做“生成日报→写入 Obsidian→可选消息推送→可选索引重建”的自动化流水线,因此不是完全无关的实现。但声明存在明显夸大或不准确之处。首先,代码并没有展示对“任意来源”或 RSS/文章等多种外部来源的通用采集能力,它只是执行某个相邻目录下的 radar 技能 main.py,数据采集能力依赖外部技能,当前代码本身只是调度器。其次,声明提到支持 cron 定时执行,但代码没有包含任何 cron 配置或触发器,只能被外部定时器调用。最后,声明说支持 Telegram / 飞书推送,但在该代码片段里推送内容来自 /tmp/radar_push_<type>.txt,而该文件并未在脚本中生成,说明至少此片段内推送链路不完整。综合判断:描述与实际行为部分一致,但存在实质性能力夸大与实现不完整,应判为 mismatch。

Credential Access

High
Category
Privilege Escalation
Content
clawhub install obsidian-inbox-pipeline

# 复制环境变量模板
cp references/.env.example .env
# 编辑 .env,填入真实路径和凭证
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
clawhub install obsidian-inbox-pipeline

# 复制环境变量模板
cp references/.env.example .env
# 编辑 .env,填入真实路径和凭证
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
```bash
# 三合一每日自动运行(配合 cron)
0 7 * * * \
  source /path/to/.env && \
  bash /path/to/obsidian-inbox-pipeline/scripts/daily_pipeline.sh \
    "ai-radar" "AI 资讯雷达" "AI" "🤖" "AI 资讯" && \
  bash /path/to/obsidian-inbox-pipeline/scripts/daily_pipeline.sh \
Confidence
77% confidence
Finding
The cron example uses `source /path/to/.env` inline before invoking shell scripts. In practice, this pattern can expose secrets to process listings, inherited environments, debugging output, or overly broad shell contexts, especially if the downstream scripts call other tools or log failures verbosely.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill documents capabilities that require access to environment variables, local files, shell execution, and potentially networked notification channels, but it does not declare any explicit tool scope or permission boundaries. This increases the risk of overbroad execution in hosts that rely on manifest-declared permissions, making unintended file access, command execution, or secret exposure harder to constrain or audit.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The manifest description and main documentation are written entirely in Chinese, which implicitly constrains the skill to a specific language/locale. The file does not offer an opt-in language choice or explain that the skill is intended only for a Chinese-language audience or region-specific use case.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The module docstring, usage instructions, argument help text, and status messaging are all written in Chinese, which imposes a specific language/locale on users. The file does not indicate that Chinese is optional, configurable, or limited to a documented region-specific use case.

Tainted flow: 'path' from os.environ.get (line 27, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
def write_note(path: str, content: str) -> str:
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, 'w', encoding='utf-8') as f:
        f.write(content)
    return path
Confidence
91% confidence
Finding
The file write path is derived from environment-controlled values via get_vault() and OBSIDIAN_INBOX_DIR, then written with open() without constraining the final path to an approved base directory. If an attacker can influence those environment variables, the skill can be abused to overwrite arbitrary files accessible to the process, which is especially relevant in an automation pipeline that runs unattended.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# fallback: 尝试 obsidian-cli
    import subprocess
    try:
        result = subprocess.run(
            ["obsidian-cli", "print-default", "--path-only"],
            capture_output=True, text=True, timeout=5
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This file contains natural-language descriptions, help text context, and output strings that assume Chinese as the required language. Under the policy, forcing a specific language without user opt-in is a natural-language policy violation.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def search_content(query: str, vault: str, limit: int = 10) -> List[Dict]:
    """用 obsidian-cli 搜索正文内容"""
    try:
        result = subprocess.run(
            ["obsidian-cli", "search-content", query, "--path", vault],
            capture_output=True, text=True, timeout=15
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The argparse description and help text are presented only in Chinese, which forces a single locale for interaction. The policy allows locale constraints only when users can opt in or when the restriction is clearly justified.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The natural-language descriptions, CLI help text, and generated report content are all hard-coded in Chinese, indicating a fixed locale experience. There is no visible option for users to select another language or any justification that this skill is intended only for a Chinese-language context.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return path
        import subprocess
        try:
            r = subprocess.run(["obsidian-cli", "print-default", "--path-only"],
                              capture_output=True, text=True, timeout=5)
            if r.returncode == 0:
                return r.stdout.strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return path
        import subprocess
        try:
            r = subprocess.run(["obsidian-cli", "print-default", "--path-only"],
                              capture_output=True, text=True, timeout=5)
            if r.returncode == 0:
                return r.stdout.strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return path
        import subprocess
        try:
            r = subprocess.run(["obsidian-cli", "print-default", "--path-only"],
                              capture_output=True, text=True, timeout=5)
            if r.returncode == 0:
                return r.stdout.strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The script constructs a Python entrypoint path from the user-supplied radar_type and executes it, enabling invocation of arbitrary sibling skill code rather than a fixed trusted program. In an agent-skill ecosystem where neighboring skills may be untrusted or less reviewed, this broadens execution scope and can lead to arbitrary code execution, data access, or unintended side effects.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
This block sends locally generated report content to Telegram using bot credentials whenever environment variables are present, with no confirmation, scoping controls, or output sanitization policy beyond Markdown escaping. That can leak sensitive vault-derived or upstream-ingested content to a third party if the pipeline processes confidential material or is run in a shared environment.

External Transmission

Medium
Category
Data Exfiltration
Content
}).encode()

req = urllib.request.Request(
    f"https://api.telegram.org/bot{token}/sendMessage",
    data=data,
    headers={"Content-Type": "application/json"}
)
Confidence
79% confidence
Finding
The script makes an external network request to Telegram's API to transmit report text off-host. In isolation, network use is not inherently malicious, but in this skill context it materially changes the data-flow trust boundary by exporting locally assembled content and therefore constitutes a real exfiltration surface.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This code authenticates with Feishu using app credentials and sends report text externally without an explicit consent or review step. In a knowledge-ingestion pipeline, the content may include sensitive internal notes or scraped material, so silent transmission increases data leakage risk and expands the impact of any upstream compromise.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This code reads sensitive bot credentials from environment variables, including TELEGRAM_BOT_TOKEN and FEISHU_APP_SECRET. Although the functions have brief internal docstrings, there is no user-facing warning, logging, or confirmation indicating that credential material is being accessed.

Missing User Warnings

Low
Confidence
89% confidence
Finding
This code performs a file write into the user's Obsidian vault by creating a review directory and saving a markdown report. Although it prints the destination after writing, the safety-relevant behavior is not disclosed before execution in the top-level docstring or gated by a confirmation prompt, so users may not realize a file will be created unless they inspect the code or use --dry-run.

Static analysis

No suspicious patterns detected.