Smail/server/hooks/base_test.go

123 lines
3.6 KiB
Go

package hooks
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestPluginAllowListRejectsUnsafeNames(t *testing.T) {
allowed := pluginAllowList([]string{"safe-plugin", "safe-plugin", "../outside", "nested/file", "*", ""})
if len(allowed) != 1 || !allowed["safe-plugin"] {
t.Fatalf("unexpected allow list: %#v", allowed)
}
}
func TestIsPluginExecutable(t *testing.T) {
if runtime.GOOS == "windows" {
if !isPluginExecutable("safe.exe", 0600) || isPluginExecutable("safe", 0600) {
t.Fatal("Windows executable validation is incorrect")
}
return
}
if !isPluginExecutable("safe", os.FileMode(0700)) || isPluginExecutable("safe.txt", os.FileMode(0700)) || isPluginExecutable("safe", os.FileMode(0600)) {
t.Fatal("Unix executable validation is incorrect")
}
}
func TestSecurePluginFile(t *testing.T) {
if runtime.GOOS == "windows" {
return
}
if !securePluginFile(0700) || securePluginFile(0775) || securePluginFile(0702) || securePluginFile(os.ModeSetuid|0700) || securePluginFile(os.ModeSetgid|0700) {
t.Fatal("plugin file permission validation is incorrect")
}
}
func TestMinimalPluginEnvUsesAllowList(t *testing.T) {
t.Setenv("SMAIL_PLUGIN_SECRET", "should-not-be-forwarded")
t.Setenv("DB_PASSWORD", "should-not-be-forwarded")
env := minimalPluginEnv("C:/smail")
values := make(map[string]string, len(env))
for _, item := range env {
parts := strings.SplitN(item, "=", 2)
if len(parts) == 2 {
values[parts[0]] = parts[1]
}
}
if values["SMAIL_ROOT"] != "C:/smail" || values["SMAIL_PLUGIN"] != "1" {
t.Fatalf("plugin identity environment is incorrect: %#v", values)
}
for _, name := range []string{"SMAIL_PLUGIN_SECRET", "DB_PASSWORD", "HOME"} {
if _, ok := values[name]; ok {
t.Fatalf("sensitive environment variable was forwarded: %s", name)
}
}
}
func TestProductionPluginIsolationPolicy(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows production policy test")
}
if _, err := newPluginLaunchSpec("plugin.exe", ".", "plugin.exe", "1.socket", nil); err == nil {
t.Fatal("Windows production plugin isolation must fail closed")
}
}
func TestPersistPluginConfigReplacesFileAtomically(t *testing.T) {
workingDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
root := t.TempDir()
if err = os.Chdir(root); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = os.Chdir(workingDirectory)
ResetHooks()
})
if err = os.Mkdir("plugins", 0700); err != nil {
t.Fatal(err)
}
ResetHooks()
if !RegisterHook("spam_block", &HookSender{name: "spam_block.exe"}) {
t.Fatal("plugin hook registration failed")
}
for _, payload := range []string{`{"url":"https://model.example.com"}`, `{"url":"https://model.example.com/v2"}`} {
if err = PersistPluginConfig("spam_block", payload); err != nil {
t.Fatal(err)
}
}
path := filepath.Join(root, "plugins", "spam_block_config.json")
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != `{"url":"https://model.example.com/v2"}` {
t.Fatalf("plugin configuration content = %q", data)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if runtime.GOOS != "windows" && info.Mode().Perm() != 0600 {
t.Fatalf("plugin configuration permissions = %o, want 600", info.Mode().Perm())
}
}
func TestRegisterHookRejectsUnsafeNames(t *testing.T) {
ResetHooks()
t.Cleanup(ResetHooks)
for _, name := range []string{"../unsafe", "nested/name", `nested\name`, "name with space", strings.Repeat("a", 65)} {
if RegisterHook(name, &HookSender{}) {
t.Fatalf("unsafe hook name was accepted: %q", name)
}
}
if !RegisterHook("safe-plugin_1", &HookSender{}) {
t.Fatal("safe hook name was rejected")
}
}