feat(discovery): STACKS_ROOT path safety check

This commit is contained in:
2026-05-22 11:45:56 +02:00
parent 13ccb79ef5
commit 897093ed1c
2 changed files with 55 additions and 0 deletions
+23
View File
@@ -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, "..")
}
+32
View File
@@ -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))
})
}
}