Back to skill

Security audit

Obsidian Vault Integration

Security checks for vulnerabilities and agentic risk

Overview

This Obsidian vault skill has a coherent purpose, but its file handling can escape the vault and read or modify other local files.

Review before installing. Use only with a trusted, non-sensitive vault until path containment is fixed, do not let untrusted prompts or note content choose filenames, and require explicit user approval before any write action.

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)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/vault-read.py:146
Finding

Arbitrary File Read Through Vault Path Traversal

Content
View full analysis
dict: """Read and parse a specific vault file.""" # Try direct path first filepath = vault / filename if not filepath.exists(): # Try discovery by type files = discover_files(vault) target_type = guess_type(filename) matches = [f for f in files if f["type"] == target_type] if matches: filepath = vault / matches[0]["path"] else: print(f"ERROR: File not found: {filename}", file=sys.stderr) sys.exit(ERR_FILE_NOT_FOUND) # Read content try: with open(filepath, 'r', encoding='utf-8') as f: ``` ### Technical Analysis The `filename` argument is controlled through the `--file` command-line option and is joined directly to the configured vault path. The resulting path is not canonicalized or checked to ensure that it remains inside the vault. A filename containing parent-directory components, such as `../../sensitive-file`, can therefore resolve outside the intended vault. An absolute filename can also cause `pathlib` to discard the vault prefix. If the resulting external path exists, the script opens it and returns its contents. A symlink located inside the vault can produce the same boundary escape if it resolves to a file outside the vault. The vault directory consequently does not function as an effective security boundary. ### Attack Path 1. The attacker obtains the ability to influence the `--file` argument passed to `vault-read.py`. 2. The attacker supplies a traversal or absolute path, such as: ```text --file ../../sensitive-file ``` 3. The expression `vault / filename` resolves to a path outside the configured vault. 4. The script verifies only that the path exists; ...[truncated 685 chars]
Remediation
View remediation
Path: vault_root = vault.resolve() requested = Path(filename) if requested.is_absolute(): raise ValueError("Absolute file paths are not allowed") filepath = (vault_root / requested).resolve(strict=True) if not filepath.is_relative_to(vault_root): raise ValueError("Requested file is outside the vault") if not filepath.is_file(): raise ValueError("Requested path is not a regular file") return filepath ``` Use this validation for every direct and discovered file before opening it. In addition: - Reject absolute paths and explicit `..` path components as early validation. - Resolve symlinks before checking containment. - Consider restricting reads to Markdown files when the requested operation only supports vault notes. - Return a controlled error without exposing unnecessary absolute filesystem paths. - Add tests covering traversal paths, absolute paths, nested valid paths, and symlinks that point outside the vault. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/vault-write.py:103
Finding

Arbitrary File Modification Through Vault Path Traversal

Content
View full analysis
Path: """Find target file by name or type metadata.""" filepath = vault / filename if filepath.exists(): return filepath # Search subdirectories for md_file in vault.rglob("*.md"): if md_file.name == filename: return md_file print(f"ERROR: File not found: {filename}", file=sys.stderr) sys.exit(ERR_FILE_NOT_FOUND) ``` The returned path is subsequently passed to write operations, including: ```python filepath = find_target_file(vault, filename) if action == "add-task": if not title: print("ERROR: --title required for add-task", file=sys.stderr) sys.exit(1) add_task(vault, filepath, title, priority, owner) elif action == "mark-done": if task_id is None: print("ERROR: --task-id required for mark-done", file=sys.stderr) sys.exit(1) mark_done(vault, filepath, task_id) elif action == "append-section": if not section or not content: print("ERROR: --section and --content required for append-section", file=sys.stderr) sys.exit(1) append_section(vault, filepath, section, content) ``` ### Technical Analysis `find_target_file()` accepts the user-controlled `--file` value and returns `vault / filename` whenever the resulting path exists. It performs no canonicalization or containment check. Parent-directory traversal and absolute paths can therefore select an existing file outside the configured vault. The selected external file is then read and rewritten by `add_task()`, `mark_done()`, or `append_section()`. The later call to `filepath.relative_to(vault)` used for output does not mitigate the vulnerability because the file modification occurs before that expression is evaluated. ...[truncated 1429 chars]
Remediation
View remediation
Path: vault_root = vault.resolve() requested = Path(filename) if requested.is_absolute() or ".." in requested.parts: raise ValueError("Invalid vault-relative filename") filepath = (vault_root / requested).resolve(strict=True) if not filepath.is_relative_to(vault_root): raise ValueError("Target file is outside the vault") if not filepath.is_file() or filepath.suffix.lower() != ".md": raise ValueError("Target must be a Markdown file") return filepath ``` Additional hardening should include: - Resolve symlinks and reject targets whose canonical location is outside the vault. - Repeat containment validation immediately before replacement to reduce time-of-check/time-of-use risk. - Restrict write operations to expected Markdown file types. - Avoid using only a basename search when multiple matching files can exist. - Ensure audit and output path calculations occur safely, but do not treat logging as an authorization control. - Add regression tests for `../` traversal, absolute paths, external symlinks, and valid nested vault paths. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/vault-write.py:78
Finding

Predictable Temporary File Allows Symlink-Based File Overwrite

Content
View full analysis
0.1: print(f"ERROR: Conflict detected - file changed since read", file=sys.stderr) sys.exit(ERR_CONFLICT) # Write to temp file first, then rename (atomic on most filesystems) temp_path = filepath.with_suffix('.tmp') with open(temp_path, 'w', encoding='utf-8') as f: f.write(content) f.flush() os.fsync(f.fileno()) temp_path.replace(filepath) except SystemExit: raise except Exception as e: print(f"ERROR: Failed to write {filepath}: {e}", file=sys.stderr) sys.exit(ERR_WRITE_FAILED) ``` ### Technical Analysis The temporary path is deterministic: the original suffix is replaced with `.tmp`. For example, writing `open-questions.md` always uses `open-questions.tmp`. The temporary file is opened with mode `w`, without exclusive creation, `O_NOFOLLOW`, or a check that the path is a regular file. If another local user or process with write access to the vault creates the predictable temporary path as a symbolic link, Python follows that link when opening it. The linked target is then truncated and overwritten with the generated vault content. The function name states that the file is written "with lock," but no `fcntl` or `msvcrt` lock is acquired. Consequently, the mtime comparison does not prevent an attacker or concurrent process from changing filesystem objects between the check and replacement. ### Attack Path 1. The attacker has local write access to the vault directory or other ...[truncated 1101 chars]
Remediation
View remediation
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • 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
Findings (9)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The code accurately matches the read/query portion of the description: it can list markdown files in a vault, read specific files, parse metadata, tasks, team members, and sections, and return structured JSON. However, the declared purpose explicitly includes write/update behavior such as writing updates back to vault files, updating open questions, adding milestones, and updating the business plan. No file-writing, editing, or persistence logic exists in this code chunk. This is a material description-behavior mismatch because a significant declared capability—writing to the vault—is absent.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The description claims both read and write access to a shared Obsidian knowledge base and suggests use cases like checking business plans, reading vault notes, or querying vault contents. This code does not provide general read/query behavior. It only supports modifying an existing Markdown file via three specific actions: add-task, mark-done, and append-section. It does read file contents internally, but only as part of performing writes, not to return vault contents or answer queries. Resource scope is consistent with an Obsidian vault, and audit logging/conflict handling are supporting details, not mismatches. The main mismatch is that the declared purpose is broader than the implemented behavior, especially on reading/querying the vault.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

This is a real path traversal/arbitrary file read issue. read_file() joins user-controlled filename to the vault path and only checks filepath.exists(), but never verifies that the resolved path stays داخل the vault root; inputs like ../../etc/passwd or symlinked paths can therefore escape the vault and be read. In this skill context, that is especially dangerous because the tool is presented as a trusted knowledge-base reader, so an agent may pass natural-language-derived filenames and unintentionally expose unrelated local secrets.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill exposes environment-variable use plus implied file read/write capabilities, but it does not declare any explicit tool scope or permissions boundary. In an agent ecosystem, that makes the effective authority ambiguous and can allow broader-than-expected access to local files in the configured vault path, increasing the chance of unintended data exposure or modification.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The trigger phrases are broad, natural-language phrases such as 'check the vault' and 'update the business plan', which can cause accidental or adversarial invocation in unrelated contexts. Because this skill includes write behavior to a shared knowledge base, unintended activation could lead to unauthorized reads, writes, or persistence of attacker-influenced content.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
86% confidence
Finding

The skill explicitly supports adding and modifying persistent content in a shared vault, which creates a session-persistence channel across agent runs. In context, persistence is the feature's purpose, but it is still security-relevant because malicious or incorrect data written to notes can influence future agent decisions, spread prompt-injection content, or tamper with shared plans/tasks.

Content

Scanner excerpt · SKILL.md (reported line 30)May include surrounding context.

Returns JSON array of tasks parsed from checkbox items, with priority (critical/important/nice), status (todo/done), owner, and title.

Add a task to the vault

bash
python scripts/vault-write.py <vault-path> --file open-questions.md --action add-task --title "New task" --priority important --owner Dave

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

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

python
def add_task(vault: Path, filepath: Path, title: str, priority: str = "important", owner: str = None):
    """Add a task to an open-questions style file."""
    content, mtime = read_file_with_lock(filepath)
    lines = content.split('\n')

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

The manifest frames this skill as reading and writing Obsidian vault files, but the implementation also reads the AGENT_NAME environment variable to record audit metadata. Accessing process environment data is not an obvious requirement of vault note manipulation and adds a capability outside the core stated purpose.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

Allowing the vault path to come from OBSIDIAN_VAULT_PATH means the caller may omit an explicit vault and the script will operate on whatever directory is present in the environment. In an agentic or shared runtime, that broadens trust to ambient configuration and can cause unintended writes to the wrong vault or another accessible directory tree.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.