mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-02 08:27:56 +01:00
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
|
|
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 {
|
||
|
|
// A backslash is a legal filename character on POSIX, not a separator
|
||
|
|
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))
|
||
|
|
})
|
||
|
|
|
||
|
|
// Music libraries symlink in folders from elsewhere; following them is intended
|
||
|
|
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()) // Windows without privileges
|
||
|
|
}
|
||
|
|
|
||
|
|
_, 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())
|
||
|
|
})
|
||
|
|
})
|