Files
vikingowl 9853a522e6 refactor(security): consolidate TOCTOU-safe path canonicalization
3c87527 added engine/paths.go:resolveCanonical, duplicating the
ancestor-walk + EvalSymlinks algorithm that already lived in
fs/guard.go:ResolveWrite. Two implementations of the same TOCTOU defense
is exactly the wrong shape for security code — a bug fix in one would
silently miss the other.

Extracts the shared algorithm to security.CanonicalizePath. Both call
sites become thin wrappers that pre-anchor relative paths against the
appropriate root (cwd for engine, workspace root for guard). The
"hit-root" defensive branch in engine's version (commented "highly
unlikely") is tightened to match guard's error behavior.

Adds focused unit tests for the helper covering existing path,
non-existent leaf, non-existent mid-component, symlinked ancestor, and
relative-path rejection.
2026-05-20 01:50:38 +02:00

78 lines
2.1 KiB
Go

package security
import (
"os"
"path/filepath"
"testing"
)
func TestCanonicalizePath_ExistingPath(t *testing.T) {
dir := t.TempDir()
got, err := CanonicalizePath(dir)
if err != nil {
t.Fatalf("CanonicalizePath: %v", err)
}
want, _ := filepath.EvalSymlinks(dir)
if got != filepath.Clean(want) {
t.Errorf("got %q, want %q", got, want)
}
}
func TestCanonicalizePath_NonExistentLeaf(t *testing.T) {
dir := t.TempDir()
leaf := filepath.Join(dir, "does-not-exist.txt")
got, err := CanonicalizePath(leaf)
if err != nil {
t.Fatalf("CanonicalizePath: %v", err)
}
canonicalDir, _ := filepath.EvalSymlinks(dir)
want := filepath.Join(canonicalDir, "does-not-exist.txt")
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestCanonicalizePath_NonExistentMidComponent(t *testing.T) {
dir := t.TempDir()
deep := filepath.Join(dir, "missing-mid", "and-leaf.txt")
got, err := CanonicalizePath(deep)
if err != nil {
t.Fatalf("CanonicalizePath: %v", err)
}
canonicalDir, _ := filepath.EvalSymlinks(dir)
want := filepath.Join(canonicalDir, "missing-mid", "and-leaf.txt")
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestCanonicalizePath_ResolvesSymlinkAncestor(t *testing.T) {
// real/<linked> where /real is a symlink to a sibling tempdir; the
// canonical form must point through the resolved target.
parent := t.TempDir()
target := filepath.Join(parent, "target")
link := filepath.Join(parent, "link")
if err := os.Mkdir(target, 0o700); err != nil {
t.Fatal(err)
}
if err := os.Symlink(target, link); err != nil {
t.Fatal(err)
}
got, err := CanonicalizePath(filepath.Join(link, "new-file.txt"))
if err != nil {
t.Fatalf("CanonicalizePath: %v", err)
}
canonicalTarget, _ := filepath.EvalSymlinks(target)
want := filepath.Join(canonicalTarget, "new-file.txt")
if got != want {
t.Errorf("got %q, want %q (symlinked parent should be resolved)", got, want)
}
}
func TestCanonicalizePath_RejectsRelativePath(t *testing.T) {
if _, err := CanonicalizePath("relative/path"); err == nil {
t.Error("expected error for relative path, got nil")
}
}