invoke-ai/InvokeAI · error · ValueError

only relative download paths accepted

Error message

only relative download paths accepted

What it means

multifile_download() resolves each part path against dest_root and rejects any resolved path that escapes dest_root via is_relative_to(). This prevents path traversal — a malicious or malformed part.path like '../../etc/passwd' from writing outside the intended destination.

Source

Thrown at invokeai/app/services/download/download_default.py:218

        on_complete: Optional[DownloadEventHandler] = None,
        on_cancelled: Optional[DownloadEventHandler] = None,
        on_error: Optional[DownloadExceptionHandler] = None,
    ) -> MultiFileDownloadJob:
        dest_root = dest.resolve()
        mfdj = MultiFileDownloadJob(dest=dest_root, id=self._next_id())
        mfdj.set_callbacks(
            on_start=on_start,
            on_progress=on_progress,
            on_complete=on_complete,
            on_cancelled=on_cancelled,
            on_error=on_error,
        )

        for part in parts:
            url = part.url
            path = (dest_root / part.path).resolve()
            if not path.is_relative_to(dest_root):
                raise ValueError("only relative download paths accepted")
            job = DownloadJob(
                source=url,
                dest=path,
                access_token=access_token or self._lookup_access_token(url),
            )
            job.id = self._next_id()  # pre-assign ID so _download_part2parent can be keyed by ID
            if part.size and part.size > 0:
                job.total_bytes = part.size
                job.expected_total_bytes = part.size
            job.canonical_url = str(url)
            mfdj.download_parts.add(job)
            self._download_part2parent[job.id] = mfdj
        if submit_job:
            self.submit_multifile_download(mfdj)
        return mfdj

    def submit_multifile_download(self, job: MultiFileDownloadJob) -> None:
        pending = sorted(job.download_parts, key=lambda j: str(j.source))

View on GitHub (pinned to 0b6a024f2f)

Solutions

  1. Inspect part.url/part.path entries and remove '..' or absolute path components from the manifest
  2. Ensure every part.path is a plain relative subpath under the intended destination
  3. Check for symlinks in dest_root causing resolve() to land outside it; use a canonical dest_root
  4. Only download multifile sets from trusted sources

Example fix

// before (manifest part)
{"url": "https://x/f", "path": "../../../etc/evil"}
// after

{"url": "https://x/f", "path": "models/evil.bin"}
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
def part_path_is_safe(dest_root: Path, part_path: str) -> bool:
    root = dest_root.resolve()
    return (root / part_path).resolve().is_relative_to(root)
# pre-check every part before calling multifile_download

Type guard

def is_safe_relative_path(p: str) -> bool:
    return not p.startswith("/") and ".." not in Path(p).parts

Try / catch

try:
    service.multifile_download(parts, dest_root)
except ValueError as e:
    if "relative download paths" in str(e):
        logger.error("manifest contains unsafe part path — refusing download")
    else:
        raise

Prevention

When it happens

Trigger: A multifile download whose part.path contains '..' segments or an absolute path that resolves outside dest_root, e.g. part.path = '/abs/dir' or 'a/../../b'.

Common situations: Downloading from untrusted model indexes/manifests with hostile relative paths, misconfigured dest roots, or symlinked destinations that resolve outside dest_root.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of invoke-ai/InvokeAI@0b6a024f2f (2026-08-29). Data as JSON: /api/errors/3ac4d5bde2b5f0db. Report an issue: GitHub.