Skip to content

emrg: a session id is walked as a path, so delete_session can delete the project tree #2439

Closed

Description

@argszero

What happens

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

  1. 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.
  2. The refusal is answered, not raised: a refused delete_session reports it in a frame
    and leaves the project tree untouched.
  3. 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.
  4. 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

  1. argszero commented on Oct 21, 2026

    @argszero
    OwnerAuthor

    Handled by #2440

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions