{"record":{"id":"d7d0e3859cfb0a24","repo":"agentscope-ai/agentscope","slug":"bubblewrap-workdir-escapes-basedir","errorCode":null,"errorMessage":"Bubblewrap workdir escapes basedir.","messagePattern":"Bubblewrap workdir escapes basedir\\.","errorType":"exception","errorClass":"PermissionError","httpStatus":null,"severity":"error","filePath":"src/agentscope/app/workspace_manager/_bubblewrap_workspace_manager.py","lineNumber":115,"sourceCode":"        self._ttl = ttl\n        self._sweep_interval = sweep_interval\n        super().__init__(isolation=isolation)\n\n        self._cache: dict[str, tuple[BubblewrapWorkspace, float]] = {}\n        self._lock = asyncio.Lock()\n        self._sweep_task: asyncio.Task[None] | None = None\n\n    def _workdir_for(self, user_id: str, workspace_id: str) -> str:\n        \"\"\"Resolve the host workdir for ``(user_id, workspace_id)``.\"\"\"\n        path = os.path.join(\n            self._basedir,\n            _safe_component(user_id),\n            _safe_component(workspace_id),\n        )\n        basedir = os.path.realpath(self._basedir)\n        real_path = os.path.realpath(path)\n        if os.path.commonpath([basedir, real_path]) != basedir:\n            raise PermissionError(\"Bubblewrap workdir escapes basedir.\")\n        return path\n\n    async def _build_and_start(\n        self,\n        *,\n        workspace_id: str,\n        user_id: str,\n        agent_id: str,\n    ) -> BubblewrapWorkspace:\n        \"\"\"Construct and initialize a Bubblewrap workspace.\"\"\"\n        del agent_id\n        workdir = self._workdir_for(user_id, workspace_id)\n        os.makedirs(workdir, mode=0o700, exist_ok=True)\n        os.chmod(workdir, 0o700)\n        ws = BubblewrapWorkspace(\n            workspace_id=workspace_id,\n            host_workdir=workdir,\n            gateway_port=self._gateway_port,","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/app/workspace_manager/_bubblewrap_workspace_manager.py#L97-L133","documentation":"Raised while computing the per-workspace directory: after realpath resolution, the path built from user_id/workspace_id components no longer lies inside basedir. This is a path-traversal guard, so it raises PermissionError even if the escape is only due to symlink resolution.","triggerScenarios":"A symlink inside basedir pointing outside it such that realpath(workdir) leaves basedir; or user_id/workspace_id components containing '../' style content that survives _safe_component sanitization.","commonSituations":"basedir itself contains a symlink (e.g. /tmp -> /private/tmp style, or a linked data volume); leftover attacker-controlled or hand-crafted symlinks in the workspace tree; unusual filesystem layouts where basedir was not realpath-normalized on the manager side.","solutions":["Ensure basedir is a real, non-symlinked directory (or pass its realpath)","Remove stray symlinks under basedir that point outside it","Sanitize user_id/workspace_id before they reach the manager if they come from untrusted input"],"exampleFix":"# before\nmgr = BubblewrapWorkspaceManager(basedir=\"/tmp/ws\")  # /tmp is a symlink on macOS\n# after\nimport os\nmgr = BubblewrapWorkspaceManager(basedir=os.path.realpath(\"/tmp/ws\"))","handlingStrategy":"try-catch","validationCode":"base = os.path.realpath(basedir)\\nwd = os.path.realpath(os.path.join(base, user_id, workspace_id))\\nassert os.path.commonpath([base, wd]) == base","typeGuard":null,"tryCatchPattern":"try:\\n    await mgr._build_and_start(...)\\nexcept PermissionError:\\n    cleanup_symlinks(basedir); retry","preventionTips":["Pass realpath(basedir) when constructing the manager","Disallow symlinks inside the workspace tree via policy/monitoring"],"tags":["path-traversal","security","bubblewrap","workspace"],"backgroundTag":"path-traversal-blocked","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}