T05 · Unauthorized Access and Privilege Escalation
- Location
- scripts/convert.py:406
- Finding
- Vault-Boundary Bypass Through Unrestricted .obsidian Copy<![CDATA[ ## Vulnerability Details **File Location**: `scripts/convert.py:406-412` **Vulnerability Type**: Symlink traversal and unsafe copying of active Obsidian content **Risk Level**: High ```python # Copy .obsidian config directory as-is (not processed as markdown content above). # This gives the output pack immediate Obsidian compatibility — open the output # folder in Obsidian and it inherits all plugins, themes, and Dataview settings. obsidian_src = vault_path / '.obsidian' if obsidian_src.exists(): shutil.copytree(obsidian_src, output_path / '.obsidian') print('Copied .obsidian config.') ``` ### Technical Analysis The converter recursively copies the source vault's entire `.obsidian` directory using `shutil.copytree()` without inspecting its entries or enforcing a source-directory boundary. By default, `shutil.copytree()` follows symbolic links rather than preserving them. A malicious or untrusted vault can therefore contain a symbolic link under `.obsidian` that points to a file or directory outside the vault. During conversion, the linked content is read with the converter process's privileges and copied into the generated ExpertPack. The operation also copies active Obsidian components, including potential `.obsidian/plugins/*/main.js` files, plugin data, themes, snippets, and workspace state. This exceeds the documented description of copying configuration. If the generated vault is subsequently opened under conditions where inherited community plugins are trusted and enabled, attacker-supplied plugin code may execute in the user's Obsidian environment. No automatic network transmission is present in the reviewed project. The direct vulnerability is unauthorized local-file inclusion in the output; possible plugin execution is a secondary consequence requiring the user to open and trust the generated vault. ### Attack Path 1. An attacker prepares or modifies an Obsidian vault sup ...[truncated 1707 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Do not copy `.obsidian/` by default. Add an explicit opt-in option such as `--copy-obsidian-config`. 2. Copy only a documented allowlist of inert configuration files instead of recursively copying the directory. 3. Exclude active or sensitive content, including: - `.obsidian/plugins/` - Plugin data directories - `.obsidian/themes/` - `.obsidian/snippets/` - Workspace and session-state files 4. Reject symbolic links at every level. Use `os.lstat()` or `Path.is_symlink()` before processing each entry. 5. Resolve every candidate source path and verify that it remains beneath the expected `.obsidian` root: ```python obsidian_root = (vault_path / '.obsidian').resolve(strict=True) def is_within_root(candidate): resolved = candidate.resolve(strict=True) try: resolved.relative_to(obsidian_root) return True except ValueError: return False ``` 6. Perform the boundary check immediately before opening each source file to reduce time-of-check/time-of-use exposure. 7. Copy regular files individually with size limits and reject device files, sockets, FIFOs, and other non-regular filesystem entries. 8. Warn users that opening an untrusted converted vault in Obsidian can activate inherited plugins or other active configuration. 9. Add automated tests covering file symlinks, directory symlinks, a symlinked `.obsidian` root, nested links, broken links, and links targeting locations outside the vault. ]]>
