{"record":{"id":"438e0ff491a40afc","repo":"bytedance/deer-flow","slug":"label-path-escapes-the-user-memory-directory-r","errorCode":null,"errorMessage":"{label} path escapes the user memory directory: {relative!r}","messagePattern":"(.+?) path escapes the user memory directory: (.+?)","errorType":"exception","errorClass":"MemoryStorageCorruption","httpStatus":null,"severity":"critical","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":260,"sourceCode":"        existing_material = {key: value for key, value in existing.items() if key not in comparison_keys}\n        if incoming_material == existing_material:\n            normalized[\"revision\"] = existing_revision\n            normalized[\"updatedAt\"] = existing.get(\"updatedAt\") or normalized[\"createdAt\"]\n        else:\n            normalized[\"revision\"] = existing_revision + 1\n            normalized[\"updatedAt\"] = now\n    if not isinstance(normalized.get(\"createdAt\"), str) or not isinstance(normalized.get(\"updatedAt\"), str):\n        raise ValueError(\"fact.createdAt and fact.updatedAt must be strings\")\n    if normalized[\"consolidatedFrom\"]:\n        normalized.setdefault(\"consolidatedAt\", normalized[\"updatedAt\"])\n    return normalized\n\n\ndef _safe_relative_path(root: Path, relative: str, *, label: str) -> Path:\n    \"\"\"Resolve an untrusted persisted relative path without leaving root.\"\"\"\n    candidate = Path(relative)\n    if candidate.is_absolute():\n        raise MemoryStorageCorruption(f\"{label} path escapes the user memory directory: {relative!r}\")\n    root_resolved = root.resolve()\n    resolved = (root / candidate).resolve()\n    try:\n        resolved.relative_to(root_resolved)\n    except ValueError as exc:\n        raise MemoryStorageCorruption(f\"{label} path escapes the user memory directory: {relative!r}\") from exc\n    return resolved\n\n\ndef _fact_title(fact: dict[str, Any]) -> str:\n    explicit = str(fact.get(\"title\") or \"\").strip()\n    if explicit:\n        return explicit.replace(\"\\n\", \" \")[:160]\n    first = str(fact.get(\"content\") or \"Memory fact\").splitlines()[0].strip()\n    return (first or \"Memory fact\")[:160]\n\n\ndef _render_fact_markdown(fact: dict[str, Any]) -> bytes:","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L242-L278","documentation":"A persisted relative path (e.g. a fact's Markdown object path) was resolved against the memory root and turned out to be absolute, so _safe_relative_path raises MemoryStorageCorruption. Persisted paths are untrusted: an absolute path would let storage read or write outside the user's memory directory, so it is treated as corruption/attack data, not normalized away.","triggerScenarios":"A fact record whose markdown path field is '/etc/passwd' or '/var/lib/data.json' - from a malicious import, a hand-crafted store file, or a producer that stored absolute paths from a different machine's layout.","commonSituations":"Migrating stores between machines where absolute layouts differ and someone 'fixed' paths absolutely; untrusted skill/extension-supplied memory files; the directory moved after paths were recorded absolutely.","solutions":["Inspect the record named in the message and rewrite the stored path as relative to the user's memory root (e.g. 'facts/abc.md').","If you cannot establish provenance, treat the record as hostile: remove it and re-create the fact, which regenerates a safe relative path.","Prevent recurrence by never writing absolute paths into memory records; log and reject them at import time."],"exampleFix":"# before (stored record): {\"markdown\": \"/home/old-user/memory/facts/abc.md\"}\n# after  (stored record): {\"markdown\": \"facts/abc.md\"}","handlingStrategy":"validation","validationCode":"from pathlib import PurePosixPath\n\ndef safe_stored_path(raw: str) -> bool:\n    p = PurePosixPath(raw)\n    return not p.is_absolute() and \"..\" not in p.parts","typeGuard":"from pathlib import PurePosixPath\n\ndef is_relative_within_root(raw: object) -> bool:\n    if not isinstance(raw, str):\n        return False\n    p = PurePosixPath(raw)\n    return not p.is_absolute() and \"..\" not in p.parts","tryCatchPattern":"try:\n    store.load(record)\nexcept MemoryStorageCorruption as exc:\n    if \"escapes the user memory directory\" in str(exc):\n        quarantine(record)  # never auto-resolve absolute paths\n    raise","preventionTips":["Only ever persist plain relative paths (e.g. 'facts/abc.md') in memory records.","Reject absolute paths and '..' segments at import time as untrusted input.","Never let external tooling rewrite stored paths to absolute forms."],"tags":["deermem","memory","path-traversal","security","data-corruption"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}