Skip to content

History scanning follows symlinks and does not validate decoded session identity #864

Description

@baron

Impact

History MCP scanning follows symlinked workspace/session paths outside the saved Workspaces tree and does not verify that a decoded session's UUID matches the requested filename/ID. A history lookup can therefore expose or misattribute another durable transcript while reporting it as the requested session/workspace.

Affected snapshot

Confirmed statically on origin/main at a094dd9a83d03820201769ebc079c49fbd2e86fc.

Reachable scenarios

  1. A workspace directory, AgentSessions component, or AgentSession-<UUID>.json beneath the history root is replaced with a symlink to readable data outside that root.
  2. Or a valid session JSON is copied under another session UUID's filename.
  3. locateSession, search enrichment, or get_session stats and reads the pathname, decodes the session, and returns its transcript without containment/no-follow checks or session.id == requested sessionID validation.

A focused test can seed a symlinked session file plus a same-format file whose embedded UUID differs from its filename.

Evidence

  • Sources/RepoPrompt/Features/AgentMode/History/HistorySessionScanner.swift:839-864,1702-1722 uses stat, which follows symlinks, to accept workspace directories and files.
  • HistorySessionScanner.swift:1225-1329 builds the requested pathname, uses Data(contentsOf:), and decodes AgentSession without verifying the embedded session ID.
  • Cache keys use standardized path strings rather than an opened descriptor/canonical authority identity.
  • The history surface is intentionally cross-workspace, but its scope is still the application's saved workspace roots and the explicitly requested session identity.

Expected behavior

History reads must remain inside the canonical saved-workspace authority, must not follow attacker-controlled symlink components, and must return only the session whose embedded identity matches the request.

Suggested direction

Open from a trusted root descriptor with no-follow component traversal, verify canonical descendant containment and regular-file identity, and validate session.id == sessionID after decode. Treat violations as explicit non-retryable authority/corruption failures.

Acceptance criteria

  • Symlinked workspace, AgentSessions, index, and session-file components cannot escape the history root.
  • A filename/embedded-session-ID mismatch is rejected and never cached under the requested ID.
  • Direct lookup, list/search enrichment, and get_session share the same authority checks.
  • Regression tests cover path escape, symlink replacement, and copied-under-another-UUID files.

Validation status

Static source review only; no history files were modified or read outside the review worktree.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:agent-modeAgent Mode runtime and interfacearea:mcpMCP server, clients, and connection visibilitybugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions