{"record":{"id":"1741ac3fab3786be","repo":"zylon-ai/private-gpt","slug":"unsafe-path-absolute","errorCode":"UNSAFE_PATH_ABSOLUTE","errorMessage":"Unsafe file path (absolute): {path!r}","messagePattern":"Unsafe file path \\(absolute\\): (.+?)","errorType":"error_code","errorClass":"SkillDomainError","httpStatus":null,"severity":"error","filePath":"private_gpt/server/skills/skills_files.py","lineNumber":112,"sourceCode":"                f\"Could not determine MIME type for file: {value.path}\",\n            )\n\n    if \"SKILL.md\" not in resolved:\n        raise SkillDomainError(\n            SkillErrorCode.MISSING_SKILL_MD, \"Upload must include a SKILL.md file.\"\n        )\n\n    return list(resolved.values())\n\n\ndef _normalize_path(path: str) -> str:\n    # Normalize backslashes to forward slashes\n    path = path.replace(\"\\\\\", \"/\")\n\n    parsed = PurePosixPath(path)\n\n    if parsed.is_absolute():\n        raise SkillDomainError(\n            SkillErrorCode.UNSAFE_PATH_ABSOLUTE,\n            f\"Unsafe file path (absolute): {path!r}\",\n        )\n\n    if \"..\" in parsed.parts:\n        raise SkillDomainError(\n            SkillErrorCode.UNSAFE_PATH_TRAVERSAL,\n            f\"Unsafe file path (path traversal): {path!r}\",\n        )\n\n    parts = list(parsed.parts)\n    if parts and parts[-1].lower() == \"skill.md\":\n        parts[-1] = \"SKILL.md\"\n    return \"/\".join(parts)\n\n\n_DEFAULT_MIME_TYPE = \"application/octet-stream\"\n","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/server/skills/skills_files.py#L94-L130","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rebuild the archive with relative paths: `cd project-root && zip -r ../skill.zip .`.","Strip leading slashes when generating entries: entry.lstrip('/').","If ingesting third-party archives, pre-scan with zipfile and reject/sanitize absolute member names before upload.","Audit the packaging step (CI script, GUI zipper) that produced absolute entry names."],"exampleFix":"# before\nzip.write('/home/user/skill/SKILL.md')  # stores absolute entry '/home/user/skill/SKILL.md'\n# after\nos.chdir('/home/user/skill')\nfor f in Path('.').rglob('*'):\n    zip.write(f, arcname=str(f))  # relative entries","handlingStrategy":"validation","validationCode":"from pathlib import PurePosixPath\n\ndef entries_are_relative(names: list[str]) -> bool:\n    return all(not PurePosixPath(n.replace('\\\\', '/')).is_absolute() for n in names)","typeGuard":"const isRelative = (entry: string): boolean =>\n  !entry.replace(/\\\\/g, '/').startsWith('/');","tryCatchPattern":null,"preventionTips":["Always create archives from inside the intended root directory.","Use arcname when calling ZipFile.write to force relative entry names.","For untrusted archives, sanitize or reject absolute members before submitting."],"tags":["security","zip-slip","path-validation","skills","upload"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}