{"record":{"id":"baf56bf547262b69","repo":"agentscope-ai/agentscope","slug":"blob-key-key-r-escapes-the-root-directory","errorCode":null,"errorMessage":"Blob key {key!r} escapes the root directory.","messagePattern":"Blob key (.+?) escapes the root directory\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/agentscope/app/rag/blob_store/_local.py","lineNumber":72,"sourceCode":"\n        Rejects keys that try to escape :attr:`_root` via ``..`` or\n        absolute paths.  Callers pick keys server-side so this is\n        defensive rather than a primary trust boundary, but we still\n        refuse to write outside the root.\n\n        Args:\n            key (`str`):\n                Backend-relative key.\n\n        Returns:\n            `Path`:\n                The absolute filesystem path within :attr:`_root`.\n        \"\"\"\n        if not key or key.startswith(\"/\") or \"..\" in Path(key).parts:\n            raise ValueError(f\"Invalid blob key: {key!r}\")\n        path = (self._root / key).resolve()\n        if self._root not in path.parents and path != self._root:\n            raise ValueError(f\"Blob key {key!r} escapes the root directory.\")\n        return path\n\n    def _key_from_uri(self, uri: str) -> str:\n        \"\"\"Extract the backend-relative key from a ``local://`` URI.\"\"\"\n        if not uri.startswith(_SCHEME):\n            raise ValueError(f\"Not a local blob URI: {uri!r}\")\n        return uri[len(_SCHEME) :]\n\n    async def write_stream(self, key: str, stream: IO[bytes]) -> str:\n        \"\"\"Copy ``stream`` into the blob at ``key`` in 1 MiB chunks.\n\n        Creates intermediate directories as needed.  Existing blobs at\n        the same key are overwritten — keys are generated server-side\n        from document ids, so collisions only happen on intentional\n        re-uploads.\n\n        Args:\n            key (`str`):","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/app/rag/blob_store/_local.py#L54-L90","documentation":"The second stage of LocalBlobStore's path guard: after resolving (self._root / key), it verifies the resolved path is still under _root. If resolution (symlinks, or clever key construction) escapes the root, it raises ValueError('Blob key ... escapes the root directory.'). This catches traversal the syntactic '..' check in the first stage misses.","triggerScenarios":"A key that resolves outside _root because _root itself is reached via a symlink, or a key whose segments symlink upward; can also fire when the key is exactly '.' (path == _root is allowed, but siblings are not) or when _root is a relative path that resolves differently than expected.","commonSituations":"Placing the blob root inside a symlinked directory (e.g. /tmp → /private/tmp on macOS) while comparing against the unresolved root; container mounts where the root path resolves to a different real path; keys containing symlinks created by other tooling.","solutions":["Ensure the store's root is canonical: pass Path(root).resolve() when constructing LocalBlobStore","Remove or account for symlinks inside the blob directory","Never let keys contain user-controlled path segments; map them to opaque ids (uuid/hash) instead","If you intentionally relocated the root, recreate the store with the new resolved path"],"exampleFix":"# before\nstore = LocalBlobStore(root=Path('/tmp/blobs'))  # /tmp may be a symlink\n\n# after\nfrom pathlib import Path\nstore = LocalBlobStore(root=Path('/tmp/blobs').resolve())","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nroot = Path(root_dir).resolve()\nstore = LocalBlobStore(root=root)  # canonical root avoids false escape errors","typeGuard":"null","tryCatchPattern":"try:\n    await store.write_stream(key, stream)\nexcept ValueError as e:\n    if 'escapes the root' in str(e):\n        ...  # reject key / fix symlinked root\n    raise","preventionTips":["Construct LocalBlobStore with an already-resolved root path","Avoid symlinks inside the blob directory","Treat 'escapes the root' as a security signal: log and reject, never rewrite keys to pass"],"tags":["blob-store","path-traversal","symlink","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}