123 lines
3.6 KiB
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")
|
|
}
|
|
}
|