9853a522e6
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.
78 lines
2.1 KiB
Go
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")
|
|
}
|
|
}
|