Skip to content

Security: WebSocket methods accept unconstrained client-supplied cwd, enabling arbitrary filesystem access #316

@PavolKum

Description

@PavolKum

All WebSocket methods that accept a cwd field pass it directly to service-layer
operations without validating it against registered project workspace roots. A
WebSocket client (authenticated or not, depending on config) can send an arbitrary
absolute path as cwd and perform filesystem writes, directory listings, git
operations, or shell spawns in any directory the server process has access to.

Affected methods

Method Impact
projects.writeFile Arbitrary filesystem write
projects.searchEntries Arbitrary directory listing / file enumeration
git.init git init in any directory
git.status, git.pull, git.checkout, git.createBranch, git.listBranches, git.createWorktree, git.removeWorktree, git.runStackedAction Git operations in arbitrary directories
terminal.open, terminal.restart Shell spawn in any existing directory
shell.openInEditor Launch editor pointed at any path

Root cause

resolveWorkspaceWritePath in apps/server/src/wsServer.ts (line 155) validates
that relativePath does not escape the supplied workspaceRoot via ../ traversal,
but trusts workspaceRoot itself completely. The ProjectWriteFileInput schema in
packages/contracts/src/project.ts only requires cwd to be a non-empty trimmed
string.

No server-side middleware or guard constrains client-supplied cwd to a known project
workspace root (ServerConfig.cwd or orchestration state).

Reproduction

  1. Start the T3 Code server (no --auth-token):
    bun run --cwd apps/server start
    
  2. Connect a WebSocket client to ws://localhost:3773.
  3. Send:
    {
      "id": "1",
      "body": {
        "_tag": "projects.writeFile",
        "cwd": "/tmp",
        "relativePath": "proof.txt",
        "contents": "written by unauthenticated ws client"
      }
    }
  4. Observe /tmp/proof.txt is created with the supplied contents.

The same pattern works with projects.searchEntries (to list /etc, ~/.ssh,
etc.) and terminal.open (to spawn a shell in any directory).

Impact

  • Local attack: Any process or browser extension that can open a WebSocket to
    localhost:3773 gets full filesystem access scoped to the server's OS user.
  • Network attack: In default web mode, the server binds to all interfaces without
    requiring an auth token. Any device on the same network can exploit this.

Suggested fix

Add a server-side guard that validates every client-supplied cwd against the set of
known project workspace roots from the orchestration read model (or against
ServerConfig.cwd). Reject requests where cwd does not match a registered project.

// Example guard (pseudocode)
function assertKnownWorkspaceRoot(cwd: string, knownRoots: Set<string>): void {
  if (!knownRoots.has(cwd)) {
    throw new RouteRequestError({
      message: "cwd does not match any registered project workspace.",
    });
  }
}

Apply this to every route handler that accepts a cwd parameter.

Environment

  • Version: 0.0.3
  • OS: Windows (also affects macOS/Linux)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions