Back to skill

Security audit

Obsidian笔记CLI

Security checks for vulnerabilities and agentic risk

Overview

This is a real Obsidian vault utility, but it can rewrite many local notes without safeguards and has some misleading command behavior.

Review this before installing if your Obsidian vault contains important personal or work notes. Use it only on a backed-up or version-controlled vault, avoid running bulk replace on untrusted vaults, and treat search and replace patterns as regular expressions even when the interface suggests otherwise.

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

Error
Location
obsidian-cli.py:145
Finding
Vault Boundary Bypass Through Symbolic-Link Writes<![CDATA[ ## Vulnerability Details **File Location**: `obsidian-cli.py:17-23` and `obsidian-cli.py:145-151` **Vulnerability Type**: Symbolic-link path traversal and unauthorized file modification **Risk Level**: High ### Vulnerable Code ```python def get_all_md_files(vault_path): """获取知识库下所有.md文件""" md_files = [] for root, dirs, files in os.walk(vault_path): # 忽略.obsidian等隐藏目录 dirs[:] = [d for d in dirs if not d.startswith('.')] for f in files: if f.endswith('.md'): md_files.append(os.path.join(root, f)) return md_files ``` ```python for file_path in md_files: content = read_file_content(file_path) if pattern.search(content): new_content = pattern.sub(new_text, content) with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) modified_count +=1 print(f"✅ 已修改: {os.path.relpath(file_path, args.vault)}") ``` ### Technical Analysis The Markdown file discovery routine accepts every directory entry whose name ends in `.md`, but it does not determine whether that entry is a symbolic link. It also does not resolve the canonical path and verify that the resolved target remains beneath the selected vault root. Python's `open()` follows symbolic links by default. Consequently, although the discovered path appears to be inside the vault, the file actually opened by `cmd_replace()` may be outside it. The initial read and subsequent write both follow the link. The tool therefore fails to enforce the vault as a filesystem security boundary. This is particularly relevant when a vault originates from an untrusted archive, source repository, shared workspace, or another user. ### Attack Path 1. An attacker creates a symbolic link inside a vault, such as: `vault/external.md -> /path/to/writable/target.md`. 2. The target is a Markdown-named file outside the vault and contains text matching the replacement expression. 3. The victim opens o ...[truncated 984 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve the vault root once with `Path(args.vault).resolve(strict=True)`. 2. Reject symbolic links by checking `Path(file_path).is_symlink()` before reading or writing. 3. Resolve every candidate path and verify that it remains inside the canonical vault root, for example with `resolved_path.relative_to(vault_root)`. 4. Confirm that each candidate is a regular file before processing it. 5. Repeat path validation immediately before writing to reduce time-of-check/time-of-use exposure. 6. Use an atomic write strategy: create a temporary regular file in the validated target directory, preserve appropriate permissions, and replace the validated file atomically. 7. Where supported, open files with no-follow semantics such as `O_NOFOLLOW` and validate the opened file descriptor with `fstat()`. 8. Abort or clearly report files that fail boundary or symbolic-link validation rather than silently processing them. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
obsidian-cli.py:138
Finding
Unconditional Regular-Expression Interpretation in Search and Bulk Replacement<![CDATA[ ## Vulnerability Details **File Location**: `obsidian-cli.py:36`, `obsidian-cli.py:138-151`, and `obsidian-cli.py:239` **Vulnerability Type**: Unsafe regular-expression handling and misleading security control **Risk Level**: Medium ### Vulnerable Code ```python def cmd_search(args): """搜索命令实现""" md_files = get_all_md_files(args.vault) keyword = args.keyword results = [] pattern = re.compile(keyword, flags=0 if args.case_sensitive else re.IGNORECASE) ``` ```python def cmd_replace(args): """批量替换命令实现""" md_files = get_all_md_files(args.vault) old_text = args.old_text new_text = args.new_text modified_count = 0 print(f"🔄 正在批量替换: \"{old_text}\" → \"{new_text}\"\n") flags = 0 if args.case_sensitive else re.IGNORECASE pattern = re.compile(old_text, flags=flags) for file_path in md_files: content = read_file_content(file_path) if pattern.search(content): new_content = pattern.sub(new_text, content) with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) ``` ```python search_parser.add_argument("--regex", action="store_true", help="启用正则表达式") ``` ### Technical Analysis The search command defines a `--regex` flag, but `cmd_search()` never checks `args.regex`. User input is always passed directly to `re.compile()`, so regular-expression processing remains enabled even when the flag is absent. The replacement command has no regex-mode switch and likewise always compiles `old_text` as a regular expression. It also passes attacker- or user-controlled `new_text` directly to `pattern.sub()`, where backslash sequences and group references have special replacement semantics. This behavior conflicts with the documented interface, which states that regular-expression matching is disabled by default. It creates the following risks: - Metacharacters in values intended to be literal can match substantially more content than e ...[truncated 1799 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. In default literal mode, compile `re.escape(keyword)` or `re.escape(old_text)` instead of raw user input. 2. Check `args.regex` explicitly in the search implementation: ```python expression = keyword if args.regex else re.escape(keyword) pattern = re.compile(expression, flags) ``` 3. Add an explicit `--regex` option to the replacement command if regex replacement is intended; otherwise perform literal replacement without the regex engine. 4. In literal replacement mode, use a callable replacement such as `pattern.sub(lambda _: new_text, content)` so backslashes and group references in replacement text remain literal. 5. Catch `re.error` and return a clear error without reading or modifying files. 6. Consider limiting accepted expression length and rejecting known high-risk patterns when regex input may come from untrusted sources. 7. For stronger denial-of-service resistance, use a regex implementation with configurable execution timeouts or run regex evaluation under enforced resource limits. 8. Add dry-run, confirmation, and backup options before bulk replacement so users can review the affected files and recover from unintended matches. ]]>
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (9)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The documented behavior does not align with the analyzed behavior, including overstated export/format support and tag-management claims. Security-relevant mismatches reduce user ability to make informed consent decisions and can hide unexpected file handling behavior, especially in a tool that directly accesses local knowledge-base contents.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill advertises direct operations on a local Obsidian vault, including search, export, and batch replacement, but it does not declare any explicit tool scope such as permissions or allowed-tools. This creates an authorization and transparency gap: an agent may gain undeclared file read/write capability over user notes, making sensitive data access and destructive edits harder to constrain or review.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The skill describes bulk replacement, tag changes, export, and cleanup of empty or duplicate notes without any warning that these operations may directly modify or delete user files. In the context of a local Obsidian vault, this is particularly risky because notes often contain high-value personal or work data and changes may be irreversible if backups are absent.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The usage example demonstrates a bulk replace command operating on the entire local knowledge base without warning that it will directly rewrite note contents. This can lead users or agent systems to trigger widespread unintended edits, corrupt notes, or alter sensitive records at scale with little friction.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
Comments, help text, and runtime messages are consistently hard-coded in Chinese, and the script explicitly sets stdout encoding behavior. This imposes a specific language/locale experience without user opt-in or any mechanism to select another language, which matches the policy's language-choice concern.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The replace command performs in-place writes to every matching markdown file in the vault, which can modify large amounts of user data irreversibly. Although progress is printed, there is no confirmation prompt, dry-run safeguard, or warning comment/docstring disclosing the destructive nature of the operation before writes occur.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The argument parser documents a '--regex' option as if regex behavior is optional, but cmd_search compiles args.keyword directly with re.compile at L040 and never checks args.regex. This creates an active documentation/interface contradiction: users are told regex must be enabled explicitly, while the implementation always enables regex semantics.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The CLI exposes a '--format' option with choices 'markdown', 'html', and 'txt', implying export conversion capability. However, cmd_export ignores args.format entirely and simply writes the original markdown content to the output path at L185-L186, so the documented capability contradicts actual behavior.

Missing User Warnings

Low
Confidence
82% confidence
Finding
The export command creates directories and writes copies of note contents to an output path, which affects user data placement on disk. While it prints destination progress, the code lacks an up-front disclosure in comments/docstrings or a stronger warning that notes will be copied out of the vault to another location.

Static analysis

No suspicious patterns detected.