Fix workspace symlink escape validation

This commit is contained in:
Virgil 2026-03-23 09:09:51 +00:00
parent 2acfc3d548
commit def6a8f402
2 changed files with 49 additions and 0 deletions

View file

@ -3,7 +3,9 @@ package workspace
import (
"crypto/sha256"
"encoding/hex"
"errors"
"os"
"path/filepath"
"strings"
"sync"
@ -204,6 +206,34 @@ func joinWithinRoot(root string, parts ...string) (string, error) {
return "", os.ErrPermission
}
func resolveWorkspacePath(rootPath, workspacePath string) error {
resolvedRoot, err := filepath.EvalSymlinks(rootPath)
if err != nil {
return err
}
resolvedPath, err := filepath.EvalSymlinks(workspacePath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
// The workspace may not exist yet during creation. Resolve the root and
// re-anchor the final entry under it so containment checks still compare
// canonical paths.
resolvedPath = filepath.Join(resolvedRoot, filepath.Base(workspacePath))
}
rel, err := filepath.Rel(resolvedRoot, resolvedPath)
if err != nil {
return err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return os.ErrPermission
}
return nil
}
func (s *Service) workspacePath(op, name string) (string, error) {
if name == "" {
return "", coreerr.E(op, "workspace name is required", os.ErrInvalid)
@ -215,6 +245,12 @@ func (s *Service) workspacePath(op, name string) (string, error) {
if core.PathDir(path) != s.rootPath {
return "", coreerr.E(op, "invalid workspace name: "+name, os.ErrPermission)
}
if err := resolveWorkspacePath(s.rootPath, path); err != nil {
if errors.Is(err, os.ErrPermission) {
return "", coreerr.E(op, "workspace path escapes root", err)
}
return "", coreerr.E(op, "failed to resolve workspace path", err)
}
return path, nil
}

View file

@ -67,6 +67,19 @@ func TestSwitchWorkspace_TraversalBlocked(t *testing.T) {
assert.Empty(t, s.activeWorkspace)
}
func TestSwitchWorkspace_SymlinkEscapeBlocked(t *testing.T) {
s, tempHome := newTestService(t)
outside := t.TempDir()
linkPath := core.Path(tempHome, ".core", "workspaces", "escaped-link")
require.NoError(t, os.Symlink(outside, linkPath))
err := s.SwitchWorkspace("escaped-link")
require.Error(t, err)
assert.ErrorIs(t, err, os.ErrPermission)
assert.Empty(t, s.activeWorkspace)
}
func TestWorkspaceFileSet_TraversalBlocked(t *testing.T) {
s, tempHome := newTestService(t)