Skip to content

fix: reject symlinks during persona memory bulk sync#91

Merged
brandwe merged 1 commit into
mainfrom
fix/persona-sync-symlink-guard
Jun 26, 2026
Merged

fix: reject symlinks during persona memory bulk sync#91
brandwe merged 1 commit into
mainfrom
fix/persona-sync-symlink-guard

Conversation

@brandwe

@brandwe brandwe commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

  • PersonaBackend.push_all now rejects symlinks under local_root and records them as errors, instead of silently following them
  • Adds a resolve() + relative_to(root) containment check on each uploaded path, matching the guards already present in push_one and pull_all
  • Adds a regression test that a symlink pointing outside local_root is not uploaded

The gap

push_all iterated local_root.rglob("*") and filtered with path.is_file() — which follows symlinks. A symlink planted in the synced memory dir (~/.claude/projects/<slug>/memory/) would pass the filter, and path.read_text() would read the symlink target and upload it to the claude_memory/ blob prefix.

That is a silent exfiltration path on an automated background sync: e.g. prompt-injection with shell access runs ln -s ~/.ssh/id_rsa .../memory/notes.md, and the next sync ships the private key to cloud storage.

The sibling methods were already hardened — push_one resolves and containment-checks its path, and pull_all rejects traversal-shaped keys. push_all was the only sync path with no symlink/containment guard; this closes that inconsistency.

Severity

Defense-in-depth. Exploitation requires something able to write into the memory dir (injection-driven shell, or a misbehaving hook), so it is not a standalone RCE — but it is a silent, automated channel that turns local file-write into cloud exfiltration, which is squarely in scope for this project.

Provenance

Ports an unmerged fix originally written on the retired pre-rename entraclaw repo, adapted to the current entrabot module path.

Validation

  • uv run --python 3.12 --extra dev python -m pytest tests/storage/test_persona.py -q — 19 passed (incl. new symlink test)
  • uv run --python 3.12 --extra dev ruff check src/entrabot/storage/persona.py tests/storage/test_persona.py — clean

Note: ruff format --check reports pre-existing drift in test_persona.py outside this change; left untouched to keep the diff minimal (consistent with #87).

PersonaBackend.push_all walked local_root with path.is_file(), which
follows symlinks — a symlink planted in the synced memory dir
(e.g. via prompt-injection shell access) would have its target read
and uploaded to the persona blob, exfiltrating arbitrary file
contents (SSH keys, tokens) into cloud storage.

push_one already resolves + containment-checks, and pull_all rejects
traversal-shaped keys; push_all was the one sync path with no guard.
Reject all symlinks explicitly and add the same resolve/relative_to
containment check, making the three sync methods consistent.

Ports an unmerged fix from the retired pre-rename entraclaw repo.
@brandwe brandwe merged commit 71a2ca8 into main Jun 26, 2026
5 checks passed
@brandwe brandwe deleted the fix/persona-sync-symlink-guard branch June 26, 2026 04:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant