T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/distill.py:541
- Finding
- Arbitrary File Overwrite Outside the Configured Obsidian Vault<![CDATA[ ## Vulnerability Details **File Location**: `scripts/distill.py:541-543` **Supporting Write Sink**: `scripts/distill.py:116-120` **Vulnerability Type**: Path traversal and unrestricted file overwrite **Risk Level**: High ### Vulnerable Code ```python def write_note(filepath: str, content: str) -> None: """Write content to filepath, creating parent dirs as needed.""" os.makedirs(os.path.dirname(filepath), exist_ok=True) with open(filepath, "w", encoding="utf-8") as f: f.write(content) logging.info(f"Saved → {filepath}") ``` ```python rel_path = args.path # ... out_path = os.path.join(vault_dir, rel_path) write_note(out_path, full_content) print(f"Persisted → {out_path}") ``` ### Technical Analysis The `persist` subcommand accepts `args.path` and joins it directly with the configured vault directory. The code does not reject absolute paths, normalize traversal components, resolve symbolic links, or verify that the resulting destination remains within the vault. Two bypass patterns are possible: 1. A traversal path such as `../../.bashrc` escapes the vault after filesystem path resolution. 2. An absolute `--path` causes `os.path.join(vault_dir, rel_path)` to discard `vault_dir` entirely. The resulting path is passed to `write_note`, which creates missing parent directories and opens the destination in `"w"` mode. This silently truncates and replaces an existing file. ### Attack Path 1. An attacker supplies or influences a request that causes the Agent to invoke the `persist` subcommand. 2. The attacker provides a path such as: ```text ../../home/user/.bashrc ``` or an absolute path such as: ```text /home/user/.config/example/config ``` 3. `os.path.join` constructs a destination that is outside the configured vault. 4. `write_note` creates parent directories where necessary. 5. The target file is opened in write mode and overwritten with attacker-influenced Markdown and frontmatter. 6. If the sel ...[truncated 1073 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Treat `--path` strictly as a vault-relative path and enforce containment before writing: 1. Reject empty and absolute paths using `os.path.isabs`. 2. Resolve the vault root and candidate destination with `os.path.realpath`. 3. Verify containment with `os.path.commonpath`. 4. Account for symbolic links by checking the resolved destination, not only the lexical path. 5. Reject destinations equal to the vault root or otherwise unsuitable as files. 6. Avoid unconditional overwrite. Use exclusive creation mode or require an explicit overwrite flag. 7. Add automated tests for `../`, nested traversal, absolute paths, and symlink escapes. Example hardening: ```python vault_root = os.path.realpath(os.path.expanduser(args.vault_dir)) if os.path.isabs(args.path): raise ValueError("--path must be relative to the vault") out_path = os.path.realpath(os.path.join(vault_root, args.path)) if os.path.commonpath([vault_root, out_path]) != vault_root: raise ValueError("--path resolves outside the configured vault") if os.path.isdir(out_path): raise ValueError("--path must identify a file") write_note(out_path, full_content) ``` For stronger overwrite protection, open new files with mode `"x"` and require a separately authorized option before replacing an existing note. ]]>
