mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-04 11:12:21 -04:00
* fix(plugins): confine plugin filesystem mounts to their root A plugin granted read-write filesystem access could escape its mount by creating a relative symlink inside it and then writing through that link, reaching any path the server process can write, including navidrome.db. wazero resolves guest paths by concatenating them onto the host root. Its WASI layer validates every path argument except the symlink target, which path_symlink forwards unvalidated by design, and fs.ValidPath splits on "/" only, so on Windows a "..\" path escapes the mount as well. Mounts now go through a jailedFS wrapper that denies symlink creation and rejects any path that is not filepath.IsLocal. That requires bypassing extism's AllowedPaths, which discards any FSConfig passed alongside it, so the mounts are built directly and applied per instance instead. Following symlinks that already exist in a mount is unchanged: music libraries rely on it, and read-only mounts already reject creating new ones. * test(plugins): guard against setting extism AllowedPaths Extracts the extism manifest construction so a test can assert AllowedPaths is never set. Setting it makes extism build its own FSConfig and discard the jailed mounts, silently restoring the symlink escape. Verified by simulating the regression: with AllowedPaths populated for plugins holding the filesystem permission, the new spec fails, as do two of the end-to-end sandbox specs.
123 lines
3.8 KiB
Go
123 lines
3.8 KiB
Go
// Test Library plugin for Navidrome plugin system integration tests.
|
|
// This plugin tests library metadata access WITH filesystem permission,
|
|
// allowing tests for both metadata and filesystem access.
|
|
// Build with: tinygo build -o ../test-library.wasm -target wasip1 -buildmode=c-shared .
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
|
)
|
|
|
|
// TestLibraryInput is the input for nd_test_library callback.
|
|
type TestLibraryInput struct {
|
|
Operation string `json:"operation"` // "get_library", "get_all_libraries", "read_file", "list_dir", "write_file", "symlink"
|
|
LibraryID int32 `json:"library_id,omitempty"`
|
|
MountPoint string `json:"mount_point,omitempty"` // For filesystem operations
|
|
FilePath string `json:"file_path,omitempty"` // For read_file operation (relative to mount point)
|
|
Content string `json:"content,omitempty"` // For write_file operation
|
|
Target string `json:"target,omitempty"` // For symlink operation
|
|
}
|
|
|
|
// TestLibraryOutput is the output from nd_test_library callback.
|
|
type TestLibraryOutput struct {
|
|
Library *host.Library `json:"library,omitempty"`
|
|
Libraries []host.Library `json:"libraries,omitempty"`
|
|
FileContent string `json:"file_content,omitempty"`
|
|
DirEntries []string `json:"dir_entries,omitempty"`
|
|
Error *string `json:"error,omitempty"`
|
|
}
|
|
|
|
// nd_test_library is the test callback that tests the library host functions.
|
|
//
|
|
//go:wasmexport nd_test_library
|
|
func ndTestLibrary() int32 {
|
|
var input TestLibraryInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
errStr := err.Error()
|
|
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
|
|
return 0
|
|
}
|
|
|
|
switch input.Operation {
|
|
case "get_library":
|
|
library, err := host.LibraryGetLibrary(input.LibraryID)
|
|
if err != nil {
|
|
errStr := err.Error()
|
|
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
|
|
return 0
|
|
}
|
|
pdk.OutputJSON(TestLibraryOutput{Library: library})
|
|
return 0
|
|
|
|
case "get_all_libraries":
|
|
libraries, err := host.LibraryGetAllLibraries()
|
|
if err != nil {
|
|
errStr := err.Error()
|
|
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
|
|
return 0
|
|
}
|
|
pdk.OutputJSON(TestLibraryOutput{Libraries: libraries})
|
|
return 0
|
|
|
|
case "read_file":
|
|
// Read a file from the mounted library directory
|
|
fullPath := filepath.Join(input.MountPoint, input.FilePath)
|
|
content, err := os.ReadFile(fullPath)
|
|
if err != nil {
|
|
errStr := err.Error()
|
|
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
|
|
return 0
|
|
}
|
|
pdk.OutputJSON(TestLibraryOutput{FileContent: string(content)})
|
|
return 0
|
|
|
|
case "list_dir":
|
|
// List files in the mounted library directory
|
|
entries, err := os.ReadDir(input.MountPoint)
|
|
if err != nil {
|
|
errStr := err.Error()
|
|
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
|
|
return 0
|
|
}
|
|
var names []string
|
|
for _, entry := range entries {
|
|
names = append(names, entry.Name())
|
|
}
|
|
pdk.OutputJSON(TestLibraryOutput{DirEntries: names})
|
|
return 0
|
|
|
|
case "write_file":
|
|
// Write a file to the mounted library directory
|
|
fullPath := filepath.Join(input.MountPoint, input.FilePath)
|
|
if err := os.WriteFile(fullPath, []byte(input.Content), 0600); err != nil {
|
|
errStr := err.Error()
|
|
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
|
|
return 0
|
|
}
|
|
pdk.OutputJSON(TestLibraryOutput{})
|
|
return 0
|
|
|
|
case "symlink":
|
|
// Create a symlink inside the mounted library directory
|
|
fullPath := filepath.Join(input.MountPoint, input.FilePath)
|
|
if err := os.Symlink(input.Target, fullPath); err != nil {
|
|
errStr := err.Error()
|
|
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
|
|
return 0
|
|
}
|
|
pdk.OutputJSON(TestLibraryOutput{})
|
|
return 0
|
|
|
|
default:
|
|
errStr := "unknown operation: " + input.Operation
|
|
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func main() {}
|