{"record":{"id":"fb5fd63bd025335d","repo":"agentscope-ai/agentscope","slug":"unsafe-upload-path-entry-path-r","errorCode":null,"errorMessage":"Unsafe upload path: {entry.path!r}","messagePattern":"Unsafe upload path: (.+?)","errorType":"exception","errorClass":"SkillUploadError","httpStatus":null,"severity":"error","filePath":"src/agentscope/app/_service/_workspace.py","lineNumber":392,"sourceCode":"        if len(entries) > MAX_FILE_COUNT:\n            raise SkillUploadError(\n                f\"A skill may hold at most {MAX_FILE_COUNT} files, \"\n                f\"got {len(entries)}.\",\n            )\n\n        total = 0\n        roots: set[str] = set()\n        for entry in entries:\n            if entry.size > MAX_FILE_BYTES:\n                raise SkillUploadError(\n                    f\"{entry.path!r} is {entry.size} bytes, over the \"\n                    f\"{MAX_FILE_BYTES}-byte per-file limit.\",\n                )\n            total += entry.size\n\n            parts = entry.path.replace(\"\\\\\", \"/\").split(\"/\")\n            if len(parts) < 2 or any(p in (\"\", \".\", \"..\") for p in parts):\n                raise SkillUploadError(f\"Unsafe upload path: {entry.path!r}\")\n            if entry.path.startswith(\"/\"):\n                raise SkillUploadError(f\"Unsafe upload path: {entry.path!r}\")\n            roots.add(parts[0])\n\n        if total > MAX_TOTAL_BYTES:\n            raise SkillUploadError(\n                f\"The upload is {total} bytes, over the \"\n                f\"{MAX_TOTAL_BYTES}-byte limit.\",\n            )\n        if len(roots) != 1:\n            raise SkillUploadError(\n                f\"A skill must be a single folder, got {sorted(roots)}.\",\n            )\n\n        root = roots.pop()\n        if not any(e.path == f\"{root}/SKILL.md\" for e in manifest.entries):\n            raise SkillUploadError(f\"No SKILL.md at the root of {root!r}.\")\n        return root","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/app/_service/_workspace.py#L374-L410","documentation":"SkillUploadError raised when a manifest entry's path, after normalizing backslashes to slashes and splitting, has fewer than 2 parts or contains an empty, '.', or '..' segment. Every path must be 'rootfolder/relative/file' so the skill lands inside a single named root; this check blocks path-traversal and degenerate relative paths.","triggerScenarios":"A manifest entry like \"SKILL.md\" (no root folder), \"a//b.txt\" or \"a/./b\" (empty/'.' part), or \"pkg/../secret.txt\" ('..' traversal). Usually produced by client code that builds paths from file.name instead of webkitRelativePath, or by maliciously crafted manifests.","commonSituations":"Using bare filenames instead of the folder-relative path from the directory picker; symlinks resolving outside the folder; hand-built manifests; sanitizing code that collapses segments to empty strings.","solutions":["Build entry.path from the file's path relative to the picked folder's parent (like webkitRelativePath), never from basename alone.","Reject entries containing '..', './', empty segments, or backslashes before submitting.","If using os.walk-like traversal, ensure the root folder name is the first path component."],"exampleFix":"# before\nentries = [UploadEntry(path=f.name, size=f.stat().st_size) for f in root.rglob(\"*\")]\n\n# after\nentries = [UploadEntry(path=str(f.relative_to(root.parent)).replace(os.sep, \"/\"), size=f.stat().st_size) for f in root.rglob(\"*\") if f.is_file()]","handlingStrategy":"validation","validationCode":"import re\ndef safe_path(p: str) -> bool:\n    parts = p.replace(\"\\\\\", \"/\").split(\"/\")\n    return len(parts) >= 2 and not any(x in (\"\", \".\", \"..\") for x in parts) and not p.startswith(\"/\")\n\nbad = [e.path for e in manifest.entries if not safe_path(e.path)]\nassert not bad, bad","typeGuard":"def safe_path(p: str) -> bool:\n    parts = p.replace(\"\\\\\", \"/\").split(\"/\")\n    return len(parts) >= 2 and not any(x in (\"\", \".\", \"..\") for x in parts) and not p.startswith(\"/\")","tryCatchPattern":null,"preventionTips":["Derive paths from the folder-relative location, never basename.","Sanitize/collapse '//' and './' before adding entries."],"tags":["upload","path-traversal","security","validation"],"backgroundTag":"unsafe-file-path","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}