{"record":{"id":"2d82f3aae3222ebd","repo":"Comfy-Org/ComfyUI","slug":"destination-escapes-base-directory","errorCode":null,"errorMessage":"destination escapes base directory","messagePattern":"destination escapes base directory","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"app/assets/services/path_utils.py","lineNumber":72,"sourceCode":"            bases = model_folder_paths[folder_name]\n        except KeyError:\n            raise ValueError(f\"unknown model category '{folder_name}'\")\n        if not bases:\n            raise ValueError(f\"no base path configured for category '{folder_name}'\")\n        base_dir = os.path.abspath(bases[0])\n    elif root == \"input\":\n        base_dir = os.path.abspath(folder_paths.get_input_directory())\n    else:\n        base_dir = os.path.abspath(folder_paths.get_output_directory())\n\n    return base_dir, []\n\n\ndef validate_path_within_base(candidate: str, base: str) -> None:\n    cand_abs = Path(os.path.abspath(candidate))\n    base_abs = Path(os.path.abspath(base))\n    if not cand_abs.is_relative_to(base_abs):\n        raise ValueError(\"destination escapes base directory\")\n\n\ndef _compute_relative_path(child: str, parent: str) -> str:\n    rel = os.path.relpath(os.path.abspath(child), os.path.abspath(parent))\n    if rel == \".\":\n        return \"\"\n    return rel.replace(os.sep, \"/\")\n\n\ndef _is_relative_to(child: str, parent: str) -> bool:\n    return Path(os.path.abspath(child)).is_relative_to(os.path.abspath(parent))\n\n\ndef compute_asset_response_paths(file_path: str) -> tuple[str, str | None] | None:\n    \"\"\"Return (logical_path, display_name) for a file path.\n\n    ``logical_path`` is the internal namespaced storage locator (e.g.\n    ``models/checkpoints/foo/bar.safetensors``); ``display_name`` is the","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/Comfy-Org/ComfyUI/blob/1c6d8d45b3693bfbb32385b410d813a7fd6be216/app/assets/services/path_utils.py#L54-L90","documentation":"Raised by validate_path_within_base when the absolute candidate path is not located under the absolute base directory (Path.is_relative_to check). It is the final containment guard on upload destinations: after joining a hashed basename onto the resolved base dir, the result must still live inside that base, blocking path-traversal writes (e.g. via '..' segments or crafted filenames).","triggerScenarios":"An upload whose client_filename/extension or tag-derived subdir produces a dest_abs outside base_dir — e.g. an extension containing path separators or '..' so os.path.join escapes the base; or a symlinked base where abspath resolution diverges. Called from ingest before any bytes are moved.","commonSituations":"Malicious or malformed filenames (ext like '/../../evil', sneaky unicode separators); mismatched path normalization between the tag resolution and the join; symlinked model/input directories making absolute-path comparison fail; security testing of the upload endpoint.","solutions":["If you're a legitimate caller: don't craft extensions/subdirs — send a plain filename and let the server derive the extension; the hashed basename makes traversal impossible in normal flow.","If you operate the server: ensure the input/output/model base directories are real directories, not symlinks whose targets confuse abspath containment checks.","For security reviewers: this guard is the last line — also check the earlier extension length cap (<=16 chars) that constrains the join input."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"# Server-side: constrain extensions to a safe charset before the join\nimport re\nSAFE_EXT = re.compile(r'^\\.[A-Za-z0-9_-]{1,15}$')\n\ndef safe_ext(filename: str) -> str:\n    ext = os.path.splitext(os.path.basename(filename))[1]\n    return ext if SAFE_EXT.match(ext) else ''","typeGuard":"def destination_is_within(dest: str, base: str) -> bool:\n    return Path(os.path.abspath(dest)).is_relative_to(Path(os.path.abspath(base)))","tryCatchPattern":"try:\n    validate_path_within_base(dest_abs, base_dir)\nexcept ValueError:\n    raise UploadRejected('invalid destination')  # do not write bytes","preventionTips":["Always basename() client filenames before deriving extensions","Reject extensions containing separators, '..', or unusual characters at the boundary","Prefer hashed basenames (as ingest does) so user input never names the file directly","Test the upload path with traversal payloads in CI"],"tags":["upload","path-traversal","security","validation"],"backgroundTag":null,"analyzedSha":"1c6d8d45b3693bfbb32385b410d813a7fd6be216","analyzedAt":"2026-08-14T19:37:18.893Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}