T05 · Unauthorized Access and Privilege Escalation
- 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]
