T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/generate_storybook_v3.py:201- Finding
Arbitrary Local File Inclusion in Generated Storybooks
- Content
View full analysis
Vulnerability Details
File Location:
scripts/generate_storybook_v3.py, lines 201–253
Vulnerability Type: Unrestricted local file read and inclusion
Risk Level: HighVulnerable Code
python def _resolve_image_path(self, image_ref: str) -> Optional[Path]: if not image_ref: return None ref_path = Path(image_ref) if ref_path.exists(): return ref_path basename = ref_path.name search_roots = [ Path.home() / ".openclaw/media/outbound/yumfu", Path.home() / ".openclaw/media/outbound", Path.home() / ".openclaw/media/tool-image-generation", Path.home() / "clawd/output", ] for root in search_roots: if not root.exists(): continue exact = root / basename if exact.exists(): return exact stem = Path(basename).stem.lower() stem_tokens = [tok for tok in stem.replace("_", "-").split("-") if tok] for root in search_roots: if not root.exists(): continue for p in root.rglob("*"): if not p.is_file(): continue p_name = p.name.lower() p_stem = p.stem.lower() if basename.lower() == p_name: return p if stem and (stem in p_stem or p_stem in stem): return p if stem_tokens and sum(1 for tok in stem_tokens if tok in p_stem) >= max(2, min(4, len(stem_tokens))): return p return None def collect_images(self, events: List[Dict[str, Any]]) -> Dict[str, Path]: image_map: Dict[str, Path] = {} for event in events: img_ref = event.get("image") or event.get("filename") if not img_ref: continue img_path = self._resolve_image_path(str(img_ref)) if img_path and img_path.exists(): dest = self.images_dir / img_path.name ...[truncated 3265 chars]- Remediation
View remediation
Remediation Suggestions
- Define a minimal allowlist of directories from which storybook images may be loaded.
- Canonicalize both the candidate and allowed roots with
Path.resolve(strict=True). - Require the canonical candidate to satisfy
candidate.is_relative_to(allowed_root)for at least one allowed root. - Reject absolute paths supplied by session data unless they resolve inside an approved root.
- Reject symbolic links or verify their fully resolved targets before opening them.
- Validate that candidates are regular files and impose a conservative maximum file size.
- Verify image content with an image decoder such as Pillow rather than relying on file extensions or
mimetypes.guess_type(). - Copy and encode only the validated canonical path.
- Treat session logs as untrusted input and validate
imageandfilenamefields when they are first written. - Add tests covering absolute paths,
../traversal, symlink escapes, non-image files, oversized files, and valid assets under every approved media root.
A secure path check should follow this pattern:
python candidate = Path(image_ref).expanduser().resolve(strict=True) allowed_roots = [root.resolve(strict=True) for root in APPROVED_MEDIA_ROOTS] if not candidate.is_file(): return None if not any(candidate.is_relative_to(root) for root in allowed_roots): return None if candidate.is_symlink(): return None
