{"record":{"id":"1f43b3536af48d92","repo":"oraios/serena","slug":"memory-name-resolves-outside-the-memories-director","errorCode":null,"errorMessage":"Memory name resolves outside the memories directory. Got: {'/'.join(parts)}","messagePattern":"Memory name resolves outside the memories directory\\. Got: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/serena/memories/memory_manager.py","lineNumber":169,"sourceCode":"        \"\"\"\n        Builds the ``*.md`` path for ``parts`` under ``base_dir``, creating any parent\n        subdirectories, and guarantees the result stays inside ``base_dir``.\n\n        The containment check is a defense-in-depth backstop for :meth:`get_memory_file_path`'s\n        up-front segment validation: even if a crafted name slipped through, the built path must\n        never escape the memories sandbox (which would let an agent read/write/delete arbitrary\n        files). The check runs *before* any directory is created, so a rejected name cannot leave\n        stray directories behind either. It is deliberately *lexical* (``normpath``, no symlink\n        resolution): directory symlinks placed inside the memories folder are a supported way to\n        share memories (e.g. a monorepo symlinking each submodule's memory dir), and those must\n        keep resolving to their targets at I/O time.\n        \"\"\"\n        filename = f\"{parts[-1]}.md\"\n        subdir = base_dir if len(parts) == 1 else base_dir.joinpath(*parts[:-1])\n        candidate = subdir / filename\n        base_norm = Path(os.path.normpath(base_dir))\n        if not Path(os.path.normpath(candidate)).is_relative_to(base_norm):\n            raise ValueError(f\"Memory name resolves outside the memories directory. Got: {'/'.join(parts)}\")\n        subdir.mkdir(parents=True, exist_ok=True)\n        return candidate\n\n    def get_memory_file_path(self, name: str) -> Path:\n        name = self._sanitize_name(name)\n        parts = name.split(\"/\")\n\n        if \"..\" in parts:\n            raise ValueError(f\"Memory name cannot contain '..' segments. Got: {name}\")\n\n        # Reject absolute names and empty path segments: pathlib discards the base directory when\n        # joined with an absolute path (e.g. \"/etc/cron.d/backdoor\" would reset to \"/etc/cron.d\"),\n        # letting a memory name escape the sandbox. A leading \"/\" produces an empty first segment.\n        if os.path.isabs(name) or \"\" in parts:\n            raise ValueError(f\"Memory name cannot be absolute or contain empty path segments. Got: {name}\")\n\n        if self._is_global(name):\n            if name == self.GLOBAL_TOPIC:","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/oraios/serena/blob/7fcbca7e62555ec2287ddb2f083caee805848ea6/src/serena/memories/memory_manager.py#L151-L187","documentation":"_resolve_memory_path normalizes the candidate path and checks it stays inside the memories base directory (relative_to). If a crafted memory name resolves outside that directory (e.g. via symlink-like or normalization tricks), ValueError is raised. It is a sandbox guard preventing path escape in memory file resolution.","triggerScenarios":"get_memory_file_path with a name whose parts, after joining to base_dir and normpath, escape the memories directory — typically names combining segments that climb out (beyond the explicit '..' check) or otherwise normalize outside the base.","commonSituations":"Programmatic callers constructing memory names from untrusted input; names containing many traversal segments or unusual separators; attempts to point a memory at an arbitrary filesystem location.","solutions":["Use a plain memory name (letters, digits, hyphens, optional topic/ subpath) that stays inside the memories directory.","Remove traversal or unusual path segments from the name before calling memory APIs.","If you need content stored elsewhere, do not route it through memories — use read_file/write_file on the absolute path directly.","Sanitize/validate externally supplied names (e.g. with a regex like ^[a-zA-Z0-9_\\-/]+$) before passing them in."],"exampleFix":"// before\nmanager.load_memory(\"../other_project/notes\")\n// after\nmanager.load_memory(\"notes\")  # or \"topic/notes\", resolved inside memories dir","handlingStrategy":"validation","validationCode":"import re\nif not re.fullmatch(r\"[A-Za-z0-9_][A-Za-z0-9_\\-/]*\", name):\n    raise ValueError(f\"unsafe memory name: {name!r}\")","typeGuard":"def is_safe_memory_name(name: str) -> bool:\n    import re\n    return bool(re.fullmatch(r\"[A-Za-z0-9_][A-Za-z0-9_\\-/]*\", name))","tryCatchPattern":"try:\n    path = manager.get_memory_file_path(name)\nexcept ValueError as e:\n    if \"outside the memories directory\" in str(e):\n        log.error(\"rejected escaping memory name: %s\", name)\n    else:\n        raise","preventionTips":["Never build memory names from raw user/LLM input without a whitelist regex","Keep names as simple topic/subtopic identifiers","Treat any normalization outside the base dir as a security signal"],"tags":["python","path-traversal","security","validation"],"backgroundTag":"path-escapes-base-directory","analyzedSha":"7fcbca7e62555ec2287ddb2f083caee805848ea6","analyzedAt":"2026-08-29T00:04:09.619Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}