diff --git a/internal/discovery/pathcheck.go b/internal/discovery/pathcheck.go new file mode 100644 index 0000000..04e8f21 --- /dev/null +++ b/internal/discovery/pathcheck.go @@ -0,0 +1,23 @@ +package discovery + +import ( + "path/filepath" + "strings" +) + +// IsInsideRoot reports whether path is the same as, or nested inside, +// root. Both are cleaned before comparison so trailing slashes, "." +// segments, and ".." escapes are handled. Prefix tricks like +// "/foo" vs "/foo-evil" are NOT considered inside. +func IsInsideRoot(root, path string) bool { + r := filepath.Clean(root) + p := filepath.Clean(path) + if p == r { + return true + } + rel, err := filepath.Rel(r, p) + if err != nil { + return false + } + return !strings.HasPrefix(rel, "..") +} diff --git a/internal/discovery/pathcheck_test.go b/internal/discovery/pathcheck_test.go new file mode 100644 index 0000000..5e08697 --- /dev/null +++ b/internal/discovery/pathcheck_test.go @@ -0,0 +1,32 @@ +package discovery_test + +import ( + "testing" + + "github.com/shcizo/package-updater/internal/discovery" + "github.com/stretchr/testify/require" +) + +func TestIsInsideRoot(t *testing.T) { + cases := []struct { + name string + root string + path string + want bool + }{ + {"direct child", "/home/shcizo/self-hosted", "/home/shcizo/self-hosted/myapp", true}, + {"nested", "/home/shcizo/self-hosted", "/home/shcizo/self-hosted/a/b/c", true}, + {"root itself", "/home/shcizo/self-hosted", "/home/shcizo/self-hosted", true}, + {"sibling", "/home/shcizo/self-hosted", "/home/shcizo/other", false}, + {"parent", "/home/shcizo/self-hosted", "/home/shcizo", false}, + {"unrelated", "/home/shcizo/self-hosted", "/etc/passwd", false}, + {"prefix-trick", "/home/shcizo/self-hosted", "/home/shcizo/self-hosted-evil", false}, + {"dotdot escape", "/home/shcizo/self-hosted", "/home/shcizo/self-hosted/../etc", false}, + {"trailing slash root", "/home/shcizo/self-hosted/", "/home/shcizo/self-hosted/x", true}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + require.Equal(t, c.want, discovery.IsInsideRoot(c.root, c.path)) + }) + } +}