T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/memory_doctor/lint.py:65- Finding
Symbolic-Link Directory Escape Enables External File Read and Overwrite
- Content
View full analysis
Vulnerability Details
File Location:
scripts/memory_doctor/adapters/auto_memory.py:83-95, 111-116;scripts/memory_doctor/lint.py:65-68;scripts/memory_doctor/snapshot.py:23-26, 38-41
Vulnerability Type: Symbolic-link following and path-boundary violation
Risk Level: HighVulnerable Code
In
scripts/memory_doctor/adapters/auto_memory.py:83-95, memory files are accepted usingos.path.isfile(), which follows symbolic links:python for entry in sorted(os.listdir(root)): if not entry.endswith(".md") or entry == INDEX_FILE: continue path = os.path.join(root, entry) if not os.path.isfile(path): continue store.units.append(self._parse_file(path, entry)) if os.path.isfile(index_path): store.index = self._parse_index(index_path) return storeIn
scripts/memory_doctor/adapters/auto_memory.py:111-116, the index is opened without verifying that it is a regular file contained within the selected directory:python def _parse_index(self, path: str) -> list[IndexEntry]: entries = [] with open(path, encoding="utf-8") as f: for line in f: m = INDEX_LINE_RE.match(line.rstrip("\n"))In
scripts/memory_doctor/lint.py:65-68,MEMORY.mdis opened directly for truncating writes:python def apply_index(store: MemoryStore, plan: IndexPlan) -> None: path = store.index_path or os.path.join(store.root, "MEMORY.md") with open(path, "w", encoding="utf-8") as f: f.write(render_index(plan))In
scripts/memory_doctor/snapshot.py:23-26, snapshot creation follows any Markdown symbolic link found in the memory directory:python for entry in os.listdir(root): if entry.endswith(".md"): shutil.copy2(os.path.join(root, entry), os.path.join(dest, entry)) return destIn
scripts/memory_doctor/snapshot.py:38-41, snapshot restoration similarly follows destination sym ...[truncated 4015 chars]- Remediation
View remediation
Remediation Suggestions
-
Reject symbolic links for all memory, index, snapshot, and restore entries. Use
os.lstat()and requirestat.S_ISREG(result.st_mode)rather than relying onos.path.isfile(). -
Enforce canonical directory containment. Resolve both the trusted root and each candidate path, then verify containment before any read or write:
python root_real = os.path.realpath(root) candidate_real = os.path.realpath(candidate) if os.path.commonpath([root_real, candidate_real]) != root_real: raise ValueError("path escapes the memory directory")Apply this validation to
MEMORY.md, every loaded memory file, snapshot sources, snapshot entries, and restore destinations. -
Use no-follow file-opening semantics. On supported platforms, open files with
os.open()andO_NOFOLLOW, validate the returned descriptor withos.fstat(), and then wrap it withos.fdopen(). This reduces symbolic-link and time-of-check/time-of-use risks. -
Write the index atomically. Create a temporary regular file inside the verified memory directory, flush and synchronize it, and then replace a verified non-link
MEMORY.mdusingos.replace(). Ensure the destination is revalidated immediately before replacement. -
Harden snapshot restoration. Permit only expected regular Markdown files, reject nested paths and links, validate both source and destination boundaries, and avoid copying through existing destination links.
-
Use restrictive permissions for snapshots. Create
.memory-doctorand snapshot directories with permissions appropriate for potentially sensitive memory content. -
Add regression tests. Cover a linked
MEMORY.md, linked ordinary Markdown files, linked snapshot entries, destination-link replacement beforeundo, external path targets, and concurrent path replacement.
-
