T09 · Insecure Skill Coding Practices
- Location
scripts/pack_openclaw.py:21- Finding
Workspace symlinks can disclose files outside the authorized export root
- Content
View full analysis
Vulnerability Details
File Location:
scripts/pack_openclaw.py:21-28,scripts/pack_openclaw.py:58-67, andscripts/pack_openclaw.py:165-170
Vulnerability Type: Missing symlink and source-path containment validation
Risk Level: HighVulnerable 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) / namepython 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 planpython 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
View remediation
Remediation Suggestions
-
Reject all symbolic-link source entries before adding them to a plan:
python if f.is_symlink() or not f.is_file(): continue -
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 -
Apply equivalent checks to workspace files, managed skills, and session files.
-
Prefer descriptor-based file opening with no-follow semantics, such as
O_NOFOLLOWon supported platforms, and archive from the validated descriptor to reduce time-of-check/time-of-use attacks. -
Report every skipped symlink prominently in both dry-run and real execution.
-
Add tests covering file symlinks to external files, broken symlinks, symlink replacement races, and links targeting
~/.openclaw/credentials/.
-
