From 454c5779684d5c7c84e51c3846af6f4fbdca49eb Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 20 Jul 2026 17:02:37 +0000 Subject: [PATCH] fix(agentcompat): secure credentialed agent workspaces Co-authored-by: naiba/CloudCode --- .../agentcompat/internal/agent/fixture.go | 29 +++++++++++++++++-- .../internal/agent/prepared_binary.go | 2 +- .../internal/agent/prepared_binary_test.go | 9 ++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/integration/agentcompat/internal/agent/fixture.go b/integration/agentcompat/internal/agent/fixture.go index a421ebc1..6ccba73d 100644 --- a/integration/agentcompat/internal/agent/fixture.go +++ b/integration/agentcompat/internal/agent/fixture.go @@ -4,9 +4,10 @@ package agent import ( "context" + "errors" "fmt" + "io/fs" "os" - "path/filepath" "github.com/nezhahq/nezha/integration/agentcompat/internal/workspace" ) @@ -96,14 +97,36 @@ func (agent *Agent) grantWorkspaceOwnership(config AgentStartConfig) error { if config.Credential == nil { return nil } - if err := filepath.WalkDir(agent.workspace.Root(), func(path string, _ os.DirEntry, walkErr error) error { + root, err := os.OpenRoot(agent.workspace.Root()) + if err != nil { + return fmt.Errorf("open agent workspace ownership root: %w", err) + } + defer root.Close() + paths := make([]string, 0) + if err := fs.WalkDir(root.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } - return os.Chown(path, int(config.Credential.Uid), int(config.Credential.Gid)) + if entry.Type()&os.ModeSymlink != 0 { + return fmt.Errorf("agent workspace symlink is not allowed: %s", path) + } + paths = append(paths, path) + return nil }); err != nil { return fmt.Errorf("grant agent workspace ownership: %w", err) } + for _, path := range paths { + file, err := root.Open(path) + if err != nil { + return fmt.Errorf("grant agent workspace ownership: %w", err) + } + // Chown the opened descriptor so a concurrent pathname swap cannot retarget ownership. + chownErr := file.Chown(int(config.Credential.Uid), int(config.Credential.Gid)) + closeErr := file.Close() + if err := errors.Join(chownErr, closeErr); err != nil { + return fmt.Errorf("grant agent workspace ownership: %w", err) + } + } return nil } diff --git a/integration/agentcompat/internal/agent/prepared_binary.go b/integration/agentcompat/internal/agent/prepared_binary.go index d499674b..766e5a26 100644 --- a/integration/agentcompat/internal/agent/prepared_binary.go +++ b/integration/agentcompat/internal/agent/prepared_binary.go @@ -55,7 +55,7 @@ func closePreparedWorkspace(workspaceRoot *workspace.Workspace, cause error) err func exposePreparedBinary(root, binaryPath string) error { for _, path := range []string{root, filepath.Dir(binaryPath)} { - if err := os.Chmod(path, 0o755); err != nil { + if err := os.Chmod(path, 0o711); err != nil { // #nosec G302 -- Directories need search permission for the credentialed test agent without granting directory reads; payloads and logs remain private. return fmt.Errorf("make prepared agent binary executable: %w", err) } } diff --git a/integration/agentcompat/internal/agent/prepared_binary_test.go b/integration/agentcompat/internal/agent/prepared_binary_test.go index 4ab3c233..646d65ad 100644 --- a/integration/agentcompat/internal/agent/prepared_binary_test.go +++ b/integration/agentcompat/internal/agent/prepared_binary_test.go @@ -22,6 +22,8 @@ func TestPreparedBinary_EightIndependentAgentsShareBinaryAndCleanUp(t *testing.T preparedRoot := prepared.WorkspaceRoot() binaryPath := prepared.BinaryPath() require.FileExists(t, binaryPath) + requireDirectoryMode(t, preparedRoot, 0o711) + requireDirectoryMode(t, filepath.Dir(binaryPath), 0o711) initialBinaryInfo, err := os.Stat(binaryPath) require.NoError(t, err) initialBinaryStat, ok := initialBinaryInfo.Sys().(*syscall.Stat_t) @@ -110,6 +112,13 @@ func TestPreparedBinary_EightIndependentAgentsShareBinaryAndCleanUp(t *testing.T require.ErrorIs(t, statErr, os.ErrNotExist) } +func requireDirectoryMode(t *testing.T, path string, want os.FileMode) { + t.Helper() + info, err := os.Stat(path) + require.NoError(t, err) + require.Equal(t, want, info.Mode().Perm()) +} + func TestAgent_StartBuildsAndOwnsItsBinary(t *testing.T) { instance, err := Start(t.Context(), AgentStartConfig{ SourceDir: testAgentSourceDir(t),