{"record":{"id":"ea7a273e7cf8a21b","repo":"invoke-ai/InvokeAI","slug":"sidecar-path-outside-outputs-folder-potential-dir","errorCode":null,"errorMessage":"Sidecar path outside outputs folder, potential directory traversal detected","messagePattern":"Sidecar path outside outputs folder, potential directory traversal detected","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"invokeai/app/services/video_files/video_files_disk.py","lineNumber":230,"sourceCode":"        if subfolder.startswith(\"/\"):\n            raise ValueError(\"Absolute paths not allowed in subfolder path\")\n        for part in subfolder.split(\"/\"):\n            if part == \"..\":\n                raise ValueError(\"Parent directory references not allowed in subfolder path\")\n            if part == \"\":\n                raise ValueError(\"Empty path segments not allowed in subfolder path\")\n\n    def __get_sidecar_path(self, video_name: str, video_subfolder: str = \"\") -> Path:\n        sidecar_name = Path(video_name).stem + \".json\"\n        if video_subfolder:\n            self._validate_subfolder(video_subfolder)\n            sidecar_path = self.__sidecars_folder / video_subfolder / sidecar_name\n        else:\n            sidecar_path = self.__sidecars_folder / sidecar_name\n        resolved_base = self.__sidecars_folder.resolve()\n        resolved_sidecar_path = sidecar_path.resolve()\n        if not resolved_sidecar_path.is_relative_to(resolved_base):\n            raise ValueError(\"Sidecar path outside outputs folder, potential directory traversal detected\")\n        return resolved_sidecar_path\n\n    def __read_sidecar(self, video_name: str, video_subfolder: str = \"\") -> Optional[dict]:\n        path = self.__get_sidecar_path(video_name, video_subfolder=video_subfolder)\n        if not path.exists():\n            return None\n        try:\n            with open(path, encoding=\"utf-8\") as f:\n                return json.load(f)\n        except Exception as e:\n            raise VideoFileNotFoundException from e\n\n    def __validate_storage_folders(self) -> None:\n        for folder in (self.__output_folder, self.__thumbnails_folder, self.__sidecars_folder):\n            folder.mkdir(parents=True, exist_ok=True)\n\n    def __recover_staged_deletes(self) -> None:\n        logger = InvokeAILogger.get_logger()","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/services/video_files/video_files_disk.py#L212-L248","documentation":"After joining the sidecars folder, subfolder, and sidecar filename, __get_sidecar_path resolves the result and verifies it is still relative to the sidecars base. If not, it means traversal escaped the outputs root (e.g. an absolute video_name or '..' that survived earlier checks, or symlink resolution). This is a defense-in-depth filesystem containment check.","triggerScenarios":"__get_sidecar_path called from save, stage_delete, __read_sidecar or __recover_staged_deletes where the composed path resolves outside the sidecars folder — e.g. video_name containing '..' (only the subfolder is validated, not the name), or a symlink pointing outward.","commonSituations":"Malicious or buggy callers passing crafted video_name values; symlinked subdirectories inside outputs; upgraded code paths that bypass _validate_subfolder.","solutions":["Ensure video_name contains no path separators or '..' before calling any sidecar operation","Check for symlinks inside the outputs/sidecars directories that resolve outside the root and remove them","Catch ValueError and surface a 4xx-style validation error to the caller instead of a 500"],"exampleFix":"// before\nsvc.save(video_name=\"../../etc/evil\", payload)\n// after\nif \"/\" in video_name or \"\\\\\" in video_name or \"..\" in video_name:\n    raise ValueError(\"invalid video name\")\nsvc.save(video_name=video_name, payload)","handlingStrategy":"validation","validationCode":"import re\nNAME_RE = re.compile(r\"^[A-Za-z0-9._-]+$\")\ndef safe_video_name(name: str) -> bool:\n    return bool(NAME_RE.match(name)) and \"..\" not in name","typeGuard":"def is_valid_video_name(name: str) -> bool:\n    return isinstance(name, str) and \"/\" not in name and \"\\\\\" not in name and \"..\" not in name","tryCatchPattern":"try:\n    sidecar = service.read_sidecar_metadata(video_name, subfolder=sub)\nexcept ValueError as e:\n    if \"directory traversal\" in str(e):\n        log.warning(\"blocked traversal attempt: %s\", video_name)\n        sidecar = None\n    else:\n        raise","preventionTips":["Validate video_name with a strict allowlist regex (no separators, no '..')","Audit outputs/sidecars directories for symlinks pointing outside","Keep subfolder and name validation centralized in one helper"],"tags":["security","path-traversal","filesystem"],"backgroundTag":"directory-traversal-blocked","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}