zylon-ai/private-gpt · error · SkillDomainError

UNSAFE_PATH_ABSOLUTE

UNSAFE_PATH_ABSOLUTE

Error message

Unsafe file path (absolute): {path!r}

What it means

SkillDomainError with code UNSAFE_PATH_ABSOLUTE, raised inside _normalize_path when a file path extracted from an upload/zip is absolute (PurePosixPath(path).is_absolute() after backslash-to-forward-slash normalization). Absolute paths are rejected because stored skills must be relocatable trees rooted at SKILL.md, and absolute entries are a classic zip-slip-adjacent pattern.

Source

Thrown at private_gpt/server/skills/skills_files.py:112

                f"Could not determine MIME type for file: {value.path}",
            )

    if "SKILL.md" not in resolved:
        raise SkillDomainError(
            SkillErrorCode.MISSING_SKILL_MD, "Upload must include a SKILL.md file."
        )

    return list(resolved.values())


def _normalize_path(path: str) -> str:
    # Normalize backslashes to forward slashes
    path = path.replace("\\", "/")

    parsed = PurePosixPath(path)

    if parsed.is_absolute():
        raise SkillDomainError(
            SkillErrorCode.UNSAFE_PATH_ABSOLUTE,
            f"Unsafe file path (absolute): {path!r}",
        )

    if ".." in parsed.parts:
        raise SkillDomainError(
            SkillErrorCode.UNSAFE_PATH_TRAVERSAL,
            f"Unsafe file path (path traversal): {path!r}",
        )

    parts = list(parsed.parts)
    if parts and parts[-1].lower() == "skill.md":
        parts[-1] = "SKILL.md"
    return "/".join(parts)


_DEFAULT_MIME_TYPE = "application/octet-stream"

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Rebuild the archive with relative paths: `cd project-root && zip -r ../skill.zip .`.
  2. Strip leading slashes when generating entries: entry.lstrip('/').
  3. If ingesting third-party archives, pre-scan with zipfile and reject/sanitize absolute member names before upload.
  4. Audit the packaging step (CI script, GUI zipper) that produced absolute entry names.

Example fix

# before
zip.write('/home/user/skill/SKILL.md')  # stores absolute entry '/home/user/skill/SKILL.md'
# after
os.chdir('/home/user/skill')
for f in Path('.').rglob('*'):
    zip.write(f, arcname=str(f))  # relative entries
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import PurePosixPath

def entries_are_relative(names: list[str]) -> bool:
    return all(not PurePosixPath(n.replace('\\', '/')).is_absolute() for n in names)

Type guard

const isRelative = (entry: string): boolean =>
  !entry.replace(/\\/g, '/').startsWith('/');

Prevention

When it happens

Trigger: A zip containing an entry like '/etc/passwd', '/tmp/x.txt', or on Windows-style archives '\\C:\\data\\file.txt' (backslashes are normalized to '/' first, making '/C:/data/file.txt' absolute). Any leading slash in an entry name triggers it.

Common situations: Zips produced by tools that store absolute paths (some tar/zip converters, PowerShell Compress-Archive with absolute inputs); malicious or corrupted archives; test fixtures hand-crafted with leading slashes.

Related errors


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/1741ac3fab3786be. Report an issue: GitHub.