Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ func bootstrapEnv(sandboxName, repoDir string, h *harness.Harness) error {
}
tmpFile.Close()

if err := sandbox.Upload(sandboxName, tmpFile.Name(), remoteEnvFile); err != nil {
if err := sandbox.UploadFile(sandboxName, tmpFile.Name(), remoteEnvFile); err != nil {
return fmt.Errorf("copying .env file to sandbox: %w", err)
}

Expand Down Expand Up @@ -998,13 +998,13 @@ func bootstrapEnv(sandboxName, repoDir string, h *harness.Harness) error {
}
tmp.Close()

if err := sandbox.Upload(sandboxName, tmp.Name(), hf.Dest); err != nil {
if err := sandbox.UploadFile(sandboxName, tmp.Name(), hf.Dest); err != nil {
os.Remove(tmp.Name())
return fmt.Errorf("copying expanded file %s to %s: %w", hf.Src, hf.Dest, err)
}
os.Remove(tmp.Name())
} else {
if err := sandbox.Upload(sandboxName, hostPath, hf.Dest); err != nil {
if err := sandbox.UploadFile(sandboxName, hostPath, hf.Dest); err != nil {
return fmt.Errorf("copying host file %s to %s: %w", hf.Src, hf.Dest, err)
}
}
Expand Down
29 changes: 29 additions & 0 deletions internal/sandbox/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,35 @@ func Upload(sandboxName, localPath, remotePath string) error {
return nil
}

// UploadFile copies a single local file into a sandbox at a specific remote path.
// openshell sandbox upload treats the remote path as a directory (similar to
// docker cp), placing the file inside it with the source basename. This function
// uploads to the parent directory and renames inside the sandbox if the resulting
// filename differs from the desired remote name, ensuring file-to-file semantics.
// This mirrors the DownloadFile approach.
func UploadFile(sandboxName, localPath, remotePath string) error {
remoteDir := filepath.Dir(remotePath)

if err := Upload(sandboxName, localPath, remoteDir); err != nil {
return err
}

// openshell places the file as <remoteDir>/<basename(localPath)>.
// If that differs from the desired remotePath, rename it in-sandbox.
uploadedPath := filepath.Join(remoteDir, filepath.Base(localPath))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] shell-quoting

UploadFile constructs the in-sandbox mv command via fmt.Sprintf("mv %s %s", ...) without shell-quoting either path. If a path contains spaces or shell metacharacters, the command will fail or behave unexpectedly. Current callers pass safe paths. This matches the pre-existing pattern in UploadDir and is not a regression.

Suggested fix: Use shell-quoted format strings (e.g. single-quote wrapping) consistent with a broader cleanup of the same pattern in UploadDir.

if uploadedPath != remotePath {
mvCmd := fmt.Sprintf("mv %s %s", uploadedPath, remotePath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] injection-defense

The mv command in UploadFile uses fmt.Sprintf without shell-quoting paths. Pre-existing pattern but not defensive against paths with spaces or metacharacters.

Suggested fix: Use shell-safe quoting for paths in sandbox Exec commands. Consider a helper function for constructing sandbox shell commands.

_, stderr, exitCode, err := Exec(sandboxName, mvCmd, 10*time.Second)
if err != nil {
return fmt.Errorf("renaming uploaded file in sandbox %q: %w", sandboxName, err)
}
if exitCode != 0 {
return fmt.Errorf("renaming uploaded file in sandbox %q: exit %d: %s", sandboxName, exitCode, stderr)
}
}
return nil
}

// UploadDir uploads a local directory into a sandbox, preserving symlinks.
// openshell sandbox upload dereferences symlinks; this builds a local tarball
// with --no-dereference, uploads it, and extracts it in the sandbox.
Expand Down
11 changes: 11 additions & 0 deletions internal/sandbox/sandbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,17 @@ func TestEffectiveReadyTimeout_EnvVarCappedAtMax(t *testing.T) {
assert.Equal(t, maxReadyTimeout, got)
}

func TestUploadFile_OpenshellNotInPath(t *testing.T) {
tmp := t.TempDir()
f := filepath.Join(tmp, "test.txt")
require.NoError(t, os.WriteFile(f, []byte("hello"), 0o644))

t.Setenv("PATH", "")

err := UploadFile("test-sandbox", f, "/tmp/workspace/test.txt")
assert.Error(t, err)
}

func TestUploadDir_OpenshellNotInPath(t *testing.T) {
dir := t.TempDir()
t.Setenv("PATH", "")
Expand Down
Loading