{"record":{"id":"67989a1f52cec98e","repo":"deepset-ai/haystack","slug":"subject-path-like-resolves-outside-the-store","errorCode":null,"errorMessage":"{subject} '{path_like}' resolves outside the store root '{root}'.","messagePattern":"(.+?) '(.+?)' resolves outside the store root '(.+?)'\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"haystack/hooks/tool_result_offloading/stores.py","lineNumber":49,"sourceCode":"        self.root = Path(root)\n\n    def _resolve_in_root(self, path_like: str | Path, *, subject: str) -> Path:\n        \"\"\"\n        Resolve a path-like value and ensure it stays within the configured store root.\n\n        Relative values are interpreted relative to `self.root`; absolute values are used as-is.\n\n        :param path_like: Relative or absolute path-like value to resolve.\n        :param subject: Human-readable label used in the error message.\n        :returns: The resolved absolute path within the store root.\n        :raises ValueError: If the resolved path escapes the store root.\n        \"\"\"\n        root = self.root.resolve()\n        path = Path(path_like)\n        candidate = path if path.is_absolute() else root / path\n        resolved = candidate.resolve()\n        if not resolved.is_relative_to(root):\n            raise ValueError(f\"{subject} '{path_like}' resolves outside the store root '{root}'.\")\n        return resolved\n\n    def write(self, *, key: str, content: str) -> str:\n        \"\"\"\n        Write `content` to `<root>/<key>`, creating parent directories, and return the file path.\n\n        The resolved target must stay within the root directory: a `key` that escapes it (e.g. containing `../` or an\n        absolute path) is rejected, so a tool-provided key cannot write outside the store.\n\n        :param key: Relative file name for the result within the store root.\n        :param content: The tool result to persist.\n        :returns: The absolute path the content was written to, as a string, for use with `read`.\n        :raises ValueError: If `key` resolves to a location outside the store root.\n        \"\"\"\n        path = self._resolve_in_root(key, subject=\"Result key\")\n        path.parent.mkdir(parents=True, exist_ok=True)\n        path.write_text(content, encoding=\"utf-8\")\n        return str(path)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/hooks/tool_result_offloading/stores.py#L31-L67","documentation":"The offloading store resolves every requested path against its root directory and rejects anything that escapes it via .., symlinks, or absolute paths pointing elsewhere. This prevents path traversal when reading or writing offloaded tool results. _resolve_in_root raises ValueError naming the offending path and the store root.","triggerScenarios":"Calling write/read with key/content paths containing `..` segments, an absolute path outside the store root, or a path whose resolved symlink target lies outside root.","commonSituations":"LLM-generated or user-supplied file keys like `../../etc/passwd`; misconfigured root that differs from where legacy data lives; symlinked directories inside the store pointing to external locations.","solutions":["Sanitize keys: use relative, flat keys without `..` or leading `/`","Pass an absolute path located inside the store root if an absolute path is required","Point the store root at the intended directory, or relocate the target file inside it"],"exampleFix":"// before\nstore.read(key=\"../../../secrets.txt\")  # ValueError\n// after\nstore.read(key=\"results/answer.txt\")  # stays inside root","handlingStrategy":"validation","validationCode":"from pathlib import Path\nroot = Path(store.root).resolve()\ncandidate = (root / key) if not Path(key).is_absolute() else Path(key)\nif not candidate.resolve().is_relative_to(root):\n    raise ValueError(f\"key {key!r} escapes store root\")","typeGuard":"from pathlib import Path\ndef is_safe_key(key: str, root: Path) -> bool:\n    p = Path(key)\n    candidate = p if p.is_absolute() else root / p\n    return candidate.resolve().is_relative_to(root.resolve())","tryCatchPattern":"try:\n    path = store.read(key=key)\nexcept ValueError as e:\n    if \"resolves outside the store root\" in str(e):\n        key = sanitize_key(key)  # strip .., absolute prefixes\n        path = store.read(key=key)\n    else:\n        raise","preventionTips":["Treat LLM/user-supplied keys as untrusted; normalize before use","Use flat, generated keys (uuid/slug) with no separators or dots","Keep symlinks out of the store root","Log rejected traversal attempts"],"tags":["python","security","path-traversal","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}