T09 · Insecure Skill Coding Practices
Error
- Location
- obsidian-cli.py:145
- Finding
- Vault Boundary Bypass Through Symbolic-Link Writes<![CDATA[ ## Vulnerability Details **File Location**: `obsidian-cli.py:17-23` and `obsidian-cli.py:145-151` **Vulnerability Type**: Symbolic-link path traversal and unauthorized file modification **Risk Level**: High ### Vulnerable Code ```python def get_all_md_files(vault_path): """获取知识库下所有.md文件""" md_files = [] for root, dirs, files in os.walk(vault_path): # 忽略.obsidian等隐藏目录 dirs[:] = [d for d in dirs if not d.startswith('.')] for f in files: if f.endswith('.md'): md_files.append(os.path.join(root, f)) return md_files ``` ```python for file_path in md_files: content = read_file_content(file_path) if pattern.search(content): new_content = pattern.sub(new_text, content) with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) modified_count +=1 print(f"✅ 已修改: {os.path.relpath(file_path, args.vault)}") ``` ### Technical Analysis The Markdown file discovery routine accepts every directory entry whose name ends in `.md`, but it does not determine whether that entry is a symbolic link. It also does not resolve the canonical path and verify that the resolved target remains beneath the selected vault root. Python's `open()` follows symbolic links by default. Consequently, although the discovered path appears to be inside the vault, the file actually opened by `cmd_replace()` may be outside it. The initial read and subsequent write both follow the link. The tool therefore fails to enforce the vault as a filesystem security boundary. This is particularly relevant when a vault originates from an untrusted archive, source repository, shared workspace, or another user. ### Attack Path 1. An attacker creates a symbolic link inside a vault, such as: `vault/external.md -> /path/to/writable/target.md`. 2. The target is a Markdown-named file outside the vault and contains text matching the replacement expression. 3. The victim opens o ...[truncated 984 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Resolve the vault root once with `Path(args.vault).resolve(strict=True)`. 2. Reject symbolic links by checking `Path(file_path).is_symlink()` before reading or writing. 3. Resolve every candidate path and verify that it remains inside the canonical vault root, for example with `resolved_path.relative_to(vault_root)`. 4. Confirm that each candidate is a regular file before processing it. 5. Repeat path validation immediately before writing to reduce time-of-check/time-of-use exposure. 6. Use an atomic write strategy: create a temporary regular file in the validated target directory, preserve appropriate permissions, and replace the validated file atomically. 7. Where supported, open files with no-follow semantics such as `O_NOFOLLOW` and validate the opened file descriptor with `fstat()`. 8. Abort or clearly report files that fail boundary or symbolic-link validation rather than silently processing them. ]]>
