T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/recap_manager.py:61- Finding
Configurable note directories can escape the configured Obsidian vault
- Content
View full analysis
Path | None: root = resolve_vault_root(config) if root is None: return None return root / Path(path) ``` ```python def daily_path(config: Dict[str, Any], date: dt.date) -> str: base = config['daily_dir'].rstrip('/') return f"{base}/{date.year}/{date.month:02d}/{date.isoformat()}.md" def weekly_path(config: Dict[str, Any], sunday: dt.date) -> str: base = config['weekly_dir'].rstrip('/') return f"{base}/{sunday.year}/{sunday.month:02d}/{sunday.isoformat()}.md" ``` ### Technical Analysis The `daily_dir` and `weekly_dir` settings can be supplied through command-line arguments or `config.json`. Their values are concatenated into note paths without rejecting absolute paths, parent-directory traversal components, or symlink-based escapes. When `path` is absolute, `root / Path(path)` discards `root` and returns the absolute path. A relative value containing `../` can similarly resolve outside the vault. The code does not call `resolve()` and does not verify that the resulting target remains beneath the resolved vault root. The affected path is subsequently used for file reads, lock-file creation, directory creation, and note overwrites. The same untrusted logical path is also passed to the configurable Obsidian CLI. ### Attack Path 1. An attacker or untrusted automation modifies local configuration or supplies an argument such as: ```bash python scripts/recap_manager.py refresh-daily-auto \ --vault-path /home/user/vault \ --daily-dir ../../external-notes \ --date 2026-03-25 ``` 2. `daily_path()` creates a path containing the unvalidated traversal sequence. 3. `note_fs_path()` joins that ...[truncated 1009 chars]- Remediation
View remediation
Path: root = resolve_vault_root(config) if root is None: raise RuntimeError("Vault root is unresolved") logical = Path(path) if logical.is_absolute() or ".." in logical.parts: raise ValueError(f"Unsafe note path: {path}") resolved_root = root.expanduser().resolve(strict=True) target = (resolved_root / logical).resolve(strict=False) if not target.is_relative_to(resolved_root): raise ValueError(f"Note path escapes vault: {path}") return target ``` 4. Perform the containment check before reads, writes, lock-file creation, or parent-directory creation. 5. Define whether symlinks are permitted. If they are not required, reject symlinked path components or verify the resolved target remains in the vault immediately before every filesystem operation. 6. Validate paths sent to the Obsidian CLI using the same policy rather than relying only on direct-filesystem fallback checks. 7. Add tests covering absolute paths, `../` traversal, nested traversal, symlink escapes, and valid in-vault paths. ]]>
