Back to skill

Security audit

Obsidian - read, search, write and edit direct to obsidian vault.

Security checks for vulnerabilities and agentic risk

Overview

This Obsidian skill does match its stated purpose, but its file handling can escape the intended vault boundary and modify persistent local files.

Install only if you are comfortable giving the skill read/write access to your Obsidian vault. Before use, restrict it to a specific vault path, avoid passing untrusted folder or note names, keep backups, and require confirmation before replace, clear, or broad edit operations.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/obsidian_cli.py:190
Finding

Vault Boundary Bypass Through Unvalidated Paths and Symlinks

Content
View full analysis
Optional[Path]: """Find note by name (with or without .md extension).""" name_clean = name.replace('.md', '') # Exact match for md in vault.rglob('*.md'): if any(p.startswith('.') for p in md.parts): continue if md.stem.lower() == name_clean.lower(): return md # Partial match for md in vault.rglob('*.md'): if any(p.startswith('.') for p in md.parts): continue if name_clean.lower() in md.stem.lower(): return md return None ``` ```python def list_notes(vault: Path, folder: str = None) -> List[Dict]: """List all notes in vault or folder.""" notes = [] search_path = vault / folder if folder else vault ``` ```python def create_note(vault: Path, title: str, content: str = '', folder: str = None, tags: list = None, auto_folder: bool = False) -> Path: """Create a new note.""" if auto_folder and not folder: folder = suggest_folder(vault, content, title) # Determine path if folder: note_dir = vault / folder note_dir.mkdir(parents=True, exist_ok=True) else: note_dir = vault # Sanitize filename safe_title = re.sub(r'[<>:"/\\|?*]', '', title) note_path = note_dir / f"{safe_title}.md" # Build frontmatter fm = { 'created': datetime.now().isoformat(), } if tags: fm['tags'] = tags full_content = format_frontmatter(fm) + content note_path.write_text(full_content, encoding='utf-8') return note_path ``` ```python elif args.command == 'read': note = find_note(vault, args.note) if not note: print(f"Note no ...[truncated 4123 chars]
Remediation
View remediation
Path: vault = path.expanduser().resolve(strict=True) if not vault.is_dir(): raise ValueError("Vault must be a directory") return vault ``` 2. Introduce a single helper that resolves candidate paths and verifies confinement: ```python def resolve_inside_vault(vault: Path, candidate: Path, *, strict: bool) -> Path: vault_root = vault.resolve(strict=True) resolved = candidate.resolve(strict=strict) if not resolved.is_relative_to(vault_root): raise ValueError("Path escapes the configured vault") return resolved ``` For Python versions without `Path.is_relative_to()`, use a guarded `relative_to()` call. 3. Reject unsafe folder input before joining it with the vault: ```python folder_path = Path(folder) if folder_path.is_absolute() or '..' in folder_path.parts: raise ValueError("Folder must be a relative path inside the vault") note_dir = resolve_inside_vault(vault, vault / folder_path, strict=False) note_dir.mkdir(parents=True, exist_ok=True) ``` 4. Validate the final note path again immediately before writing. This reduces time-of-check/time-of-use exposure and protects against symlinked parent directories: ```python note_path = resolve_inside_vault( vault, note_dir / f"{safe_title}.md", strict=False, ) ``` 5. Reject symlinked notes, or explicitly validate their resolved targets before every read and write: ```python if note_path.is_symlink(): raise ValueError("Symlinked notes are not allowed") resolved_note = resolve_inside_vault(vault, note_path, strict=True) ``` 6. Apply the same confinement helper consistently to `list_notes()`, `find_note()`, `create_note()`, `edit_note()`, and every other filesystem operation. 7. Avoid overwriting existing notes by default. Use exclusive creation mode or require an explicit overwrite flag: ```python with no ...[truncated 409 chars]
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • 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
Findings (11)

Tp4

High
Category
MCP Tool Poisoning
Confidence
90% confidence
Finding

The documented behavior relies on external scripts not included here and references concrete local filesystem access through a fixed vault path and shell execution, which is broader and less auditable than the description implies. When behavior is delegated to unseen modules, reviewers cannot verify whether search-only functionality also performs writes, path traversal, or other unsafe operations.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The documented behavior relies on external scripts not included here and references concrete local filesystem access through a fixed vault path and shell execution, which is broader and less auditable than the description implies. When behavior is delegated to unseen modules, reviewers cannot verify whether search-only functionality also performs writes, path traversal, or other unsafe operations.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
95% confidence
Finding

The list operation builds search_path as vault / folder and then traverses it without verifying that the resolved path stays inside the vault. An attacker or untrusted caller can supply absolute paths or traversal sequences like ../../ to enumerate markdown files outside the Obsidian vault, exposing arbitrary filesystem content the process can read.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

The create operation accepts a user-controlled folder and writes note_path = note_dir / f"{safe_title}.md" after creating directories, but it never constrains note_dir to remain inside the vault. A caller can provide traversal or absolute paths to create or overwrite markdown files outside the intended knowledge-base directory anywhere the process has write access.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
97% confidence
Finding

The skill documents shell commands, environment-variable usage, and direct read/write access to a local vault path, but it declares no explicit tool scope or permissions boundaries. This is dangerous because an agent may be allowed to invoke filesystem and shell capabilities without transparent least-privilege constraints, increasing the chance of unintended file access or modification.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill supports create and edit operations on a real user vault, but the documentation does not prominently warn that using it can modify existing notes and create new files. In an agent setting, missing mutation warnings can lead to unsafe invocation, accidental data corruption, or surprising persistence of sensitive content.

Content

No source excerpt is available for this finding.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

This code performs direct file overwrites for multiple edit actions, including 'replace' and 'clear', via write_text on the target note. Although editing notes is part of the tool's purpose, there is no user-facing warning or confirmation for irreversible actions such as clearing or replacing content, and the surrounding help text does not disclose that these actions overwrite existing note contents.

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/obsidian_search.py (reported line 81)May include surrounding context.

python
try:
        # Search for any of the query words (case-insensitive)
        pattern = '|'.join(re.escape(w) for w in words)
        result = subprocess.run(
            ['rg', '-l', '-i', '--type', 'md', pattern, vault_path],
            capture_output=True, text=True, timeout=10
        )

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/obsidian_search.py (reported line 93)May include surrounding context.

python
# Fallback to grep
    try:
        pattern = '|'.join(re.escape(w) for w in words)
        result = subprocess.run(
            ['grep', '-ril', '-E', pattern, '--include=*.md', vault_path],
            capture_output=True, text=True, timeout=30
        )

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

The skill's prompt examples are written only in Russian, which signals a language-specific interaction pattern without documenting user choice or opt-in. Under the language/locale policy, skills should not implicitly force a specific language unless the constraint is clearly documented and justified.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.