package plugins
import (
"os"
"path/filepath"
"runtime"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/experimental/sysfs"
)
var _ = Describe("buildMounts", func() {
var libraries model.Libraries
BeforeEach(func() {
libraries = model.Libraries{
{ID: 1, Path: "/music/library1"},
{ID: 2, Path: "/music/library2"},
{ID: 3, Path: "/music/library3"},
}
})
It("mounts all libraries read-only by default", func() {
Expect(buildMounts(nil, libraries, nil, true, false)).To(Equal([]mount{
{hostPath: "/music/library1", guestPath: "/libraries/1", readOnly: true},
{hostPath: "/music/library2", guestPath: "/libraries/2", readOnly: true},
{hostPath: "/music/library3", guestPath: "/libraries/3", readOnly: true},
}))
})
It("mounts only the selected libraries", func() {
Expect(buildMounts(nil, libraries, []int{1, 3}, false, false)).To(Equal([]mount{
{hostPath: "/music/library1", guestPath: "/libraries/1", readOnly: true},
{hostPath: "/music/library3", guestPath: "/libraries/3", readOnly: true},
}))
})
It("mounts writable when write access is granted", func() {
Expect(buildMounts(nil, libraries, []int{2}, false, true)).To(Equal([]mount{
{hostPath: "/music/library2", guestPath: "/libraries/2", readOnly: false},
}))
})
DescribeTable("mounts nothing",
func(libs model.Libraries, allowedIDs []int, allLibraries bool) {
Expect(buildMounts(nil, libs, allowedIDs, allLibraries, false)).To(BeEmpty())
},
Entry("when no library matches", libraries, []int{99}, false),
Entry("when there are no libraries", model.Libraries(nil), []int{1}, false),
Entry("when no library is selected", libraries, nil, false),
)
})
var _ = Describe("jailedFS", func() {
var (
fs jailedFS
root string
outsideDir string
)
BeforeEach(func() {
tmpDir := GinkgoT().TempDir()
root = filepath.Join(tmpDir, "root")
outsideDir = filepath.Join(tmpDir, "outside")
Expect(os.MkdirAll(root, 0755)).To(Succeed())
Expect(os.MkdirAll(outsideDir, 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(outsideDir, "secret.txt"), []byte("secret"), 0600)).To(Succeed())
fs = jailedFS{sysfs.DirFS(root)}
})
It("denies creating symlinks", func() {
Expect(fs.Symlink("../outside", "link")).To(Equal(experimentalsys.EPERM))
})
It("denies reading through a path that escapes the root", func() {
_, errno := fs.OpenFile("../outside/secret.txt", experimentalsys.O_RDONLY, 0)
Expect(errno).To(Equal(experimentalsys.EPERM))
})
It("denies traversal with the host separator", func() {
_, errno := fs.OpenFile(`..\outside\secret.txt`, experimentalsys.O_RDONLY, 0)
if runtime.GOOS == "windows" {
Expect(errno).To(Equal(experimentalsys.EPERM))
} else {
Expect(errno).To(Equal(experimentalsys.ENOENT))
}
})
It("denies absolute paths", func() {
_, errno := fs.OpenFile("/etc/passwd", experimentalsys.O_RDONLY, 0)
Expect(errno).To(Equal(experimentalsys.EPERM))
})
It("denies creating a directory outside the root", func() {
Expect(fs.Mkdir("../outside/new-dir", 0755)).To(Equal(experimentalsys.EPERM))
Expect(filepath.Join(outsideDir, "new-dir")).ToNot(BeADirectory())
})
It("denies escaping through either side of a rename", func() {
Expect(fs.Rename("../outside/secret.txt", "stolen.txt")).To(Equal(experimentalsys.EPERM))
Expect(fs.Rename("inside.txt", "../outside/leaked.txt")).To(Equal(experimentalsys.EPERM))
})
It("follows symlinks that already exist in the mount", func() {
if err := os.Symlink(outsideDir, filepath.Join(root, "linked")); err != nil {
Skip("cannot create symlinks here: " + err.Error())
}
_, errno := fs.OpenFile("linked/secret.txt", experimentalsys.O_RDONLY, 0)
Expect(errno).To(BeZero())
})
It("allows access within the root", func() {
Expect(fs.Mkdir("sub", 0755)).To(BeZero())
f, errno := fs.OpenFile("sub/file.txt", experimentalsys.O_CREAT|experimentalsys.O_WRONLY, 0600)
Expect(errno).To(BeZero())
Expect(f.Close()).To(BeZero())
Expect(filepath.Join(root, "sub", "file.txt")).To(BeAnExistingFile())
})
})