You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
emrg: a session id is walked as a path, so delete_session can delete the project tree #2439
A session id arrives in a client message and is pasted onto <cwd>/.emrg/sessions/ with
no check that it is a single path component. The kernel resolves .. and an absolute id
at that point, so the id can name something other than a session directory — and delete_session hands what it resolved to shutil.rmtree.
Measured 2026-10-21 against Session.delete("../..", <project>), in a throwaway tree that
held .emrg/sessions/keep/ and victim/precious.txt:
before the call: victim/precious.txt present
Session.delete("../..", <project>) walked <project>/.emrg/sessions/../.. — the project
tree itself — and removed its contents: victim/ and .emrg/ both gone
the call then raised FileNotFoundError from shutil.rmtree's final os.rmdir, so the
error arrives after the deletion, not instead of it
Session.delete is the static delete in emrg/session.py; the daemon's delete_session
handler (emrg/server/daemon.py) reads session_id out of the message, builds the same
join for its not-found message, and reaches that method.
The same join, without the deletion
<cwd>/.emrg/sessions/<session_id> is also built by _get_or_create_session (eleven call
sites, and it creates the directory), by _handle_resume_session's meta read, and by
the two session-scope memory readers (memories_list, memory_content). One message
field feeds all of them.
What the fix must satisfy
A session id that is not exactly one path component is refused before a path is
built: empty, ., .., anything carrying / or \, a NUL byte, and a
drive-relative id such as C:evil — a path on Windows, one component nowhere.
The refusal is answered, not raised: a refused delete_session reports it in a frame
and leaves the project tree untouched.
The predicate is asked at one seam every message crosses, so a handler added later
inherits it, and Session.delete asks it as well — it is a public static method, and a
caller outside the daemon never crosses the message seam.
A legitimate id still deletes its own directory, and a legitimate session still opens.
Reachability
The socket is loopback-only and token-authenticated, so this needs a local client — one
whose own bug passes a path-shaped session name where an id belongs is enough, and the
cost of that bug is the project directory.
Activity
argszero commentedon Oct 21, 2026
Handled by #2440