T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/pack_openclaw.py:21
- Finding
- Workspace symlinks can disclose files outside the authorized export root## Vulnerability Details **File Location**: `scripts/pack_openclaw.py:21-28`, `scripts/pack_openclaw.py:58-67`, and `scripts/pack_openclaw.py:165-170` **Vulnerability Type**: Missing symlink and source-path containment validation **Risk Level**: High ### Vulnerable Code ```python def iter_files(root: Path, *, exclude_git: bool) -> Iterable[Path]: root = root.resolve() always_skip = {"__pycache__", ".venv", "node_modules"} for dirpath, dirnames, filenames in os.walk(root): remove = [d for d in dirnames if d in always_skip or (d == ".git" and exclude_git)] for d in remove: dirnames.remove(d) for name in filenames: yield Path(dirpath) / name ``` ```python def plan_workspace(root: Path, *, exclude_git: bool) -> PackPlan: plan = PackPlan() root = root.resolve() if not root.is_dir(): plan.warnings.append(f"missing workspace: {root}") return plan prefix = "workspace/" for f in iter_files(root, exclude_git=exclude_git): if not f.is_file(): continue try: rel = f.relative_to(root) except ValueError: continue arc = prefix + rel.as_posix() plan.entries.append((f, arc)) return plan ``` ```python for abs_path, arcname in entries: if not abs_path.is_file(): continue if arcname in seen: continue seen.add(arcname) zf.write(abs_path, arcname) ``` ### Technical Analysis The exporter confirms only that the directory entry appears beneath the workspace path. It does not reject symbolic links or verify that the resolved target of every source file remains beneath the resolved workspace root. `Path.is_file()` follows symbolic links. Likewise, `ZipFile.write()` opens the referenced file and archives its contents. Consequently, a symbolic link located inside the workspace can point to ...[truncated 2117 chars]
- Remediation
- ## Remediation Suggestions 1. Reject all symbolic-link source entries before adding them to a plan: ```python if f.is_symlink() or not f.is_file(): continue ``` 2. Resolve every candidate and enforce containment under the authorized root: ```python root_resolved = root.resolve() source_resolved = f.resolve(strict=True) try: source_resolved.relative_to(root_resolved) except ValueError: plan.warnings.append(f"outside source root via symlink: {f}") continue ``` 3. Apply equivalent checks to workspace files, managed skills, and session files. 4. Prefer descriptor-based file opening with no-follow semantics, such as `O_NOFOLLOW` on supported platforms, and archive from the validated descriptor to reduce time-of-check/time-of-use attacks. 5. Report every skipped symlink prominently in both dry-run and real execution. 6. Add tests covering file symlinks to external files, broken symlinks, symlink replacement races, and links targeting `~/.openclaw/credentials/`.
