From f2d675fb55816d2570013b24edad913efb3304a7 Mon Sep 17 00:00:00 2001 From: smail Date: Tue, 22 Sep 2026 02:30:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8F=92=E4=BB=B6=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=AE=89=E5=85=A8=E5=88=9B=E5=BB=BA=E4=B8=8E=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=AB=9E=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/hooks/bundle.go | 34 +++++++++++++++- backend/hooks/plugin_isolation_linux.go | 52 +++++++++++++++++++------ 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/backend/hooks/bundle.go b/backend/hooks/bundle.go index 5983a48..d19aa9f 100644 --- a/backend/hooks/bundle.go +++ b/backend/hooks/bundle.go @@ -1,7 +1,9 @@ package hooks import ( + "bytes" "errors" + "io" "io/fs" "os" "path/filepath" @@ -43,10 +45,40 @@ func pluginRoot() (string, error) { if err != nil || len(data) == 0 { return "", errors.New("内置插件内容不可用") } - if err := os.WriteFile(filepath.Join(root, entry.Name()), data, 0700); err != nil { + if err := writeBundledPlugin(filepath.Join(root, entry.Name()), data); err != nil { return "", err } } bundledRoot = root return root, nil } + +func writeBundledPlugin(path string, data []byte) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return err + } + remove := true + defer func() { + if remove { + _ = os.Remove(path) + } + }() + if _, err := io.Copy(file, bytes.NewReader(data)); err != nil { + _ = file.Close() + return err + } + if err := file.Sync(); err != nil { + _ = file.Close() + return err + } + if err := file.Chmod(0700); err != nil { + _ = file.Close() + return err + } + if err := file.Close(); err != nil { + return err + } + remove = false + return nil +} diff --git a/backend/hooks/plugin_isolation_linux.go b/backend/hooks/plugin_isolation_linux.go index 2644463..975738c 100644 --- a/backend/hooks/plugin_isolation_linux.go +++ b/backend/hooks/plugin_isolation_linux.go @@ -42,7 +42,7 @@ func newPluginLaunchSpec(pluginPath, root, name, socketName string, env []string return nil, fmt.Errorf("create plugin sandbox: %w", err) } cleanup := func() { _ = os.RemoveAll(runtimeDir) } - if err := os.Chmod(runtimeDir, 0700); err != nil { + if err := unix.Chmod(runtimeDir, 0700); err != nil { cleanup() return nil, fmt.Errorf("secure plugin sandbox: %w", err) } @@ -93,8 +93,12 @@ func newPluginLaunchSpec(pluginPath, root, name, socketName string, env []string listenerFile, }, Sys: &syscall.SysProcAttr{ - Chroot: runtimeDir, - Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid), NoSetGroups: true}, + Chroot: runtimeDir, + Credential: &syscall.Credential{ + Uid: uint32(uid), // #nosec G115 -- pluginIdentity restricts both values to 1..65534. + Gid: uint32(gid), // #nosec G115 -- pluginIdentity restricts both values to 1..65534. + NoSetGroups: true, + }, Cloneflags: cloneFlags, Pdeathsig: syscall.SIGKILL, Setsid: true, @@ -126,12 +130,12 @@ func copyPluginConfig(root, runtimeDir, name string, uid, gid int) error { } func copyPluginFile(sourcePath, targetPath string, uid, gid int, mode os.FileMode) error { - source, err := os.Open(sourcePath) + source, err := openPluginSource(sourcePath) if err != nil { return fmt.Errorf("open plugin configuration: %w", err) } defer source.Close() - target, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode) + target, err := createPluginTarget(targetPath, mode) if err != nil { return fmt.Errorf("create sandbox plugin configuration: %w", err) } @@ -149,15 +153,41 @@ func copyPluginFile(sourcePath, targetPath string, uid, gid int, mode os.FileMod _ = os.Remove(targetPath) return errors.New("plugin configuration exceeds the allowed size") } - if err := os.Chmod(targetPath, mode); err != nil { + if err := target.Chmod(mode); err != nil { return fmt.Errorf("secure sandbox plugin configuration: %w", err) } - if err := os.Chown(targetPath, uid, gid); err != nil { + if err := target.Chown(uid, gid); err != nil { return fmt.Errorf("assign sandbox plugin configuration identity: %w", err) } return nil } +func openPluginSource(path string) (*os.File, error) { + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) + if err != nil { + return nil, err + } + file := os.NewFile(uintptr(fd), path) + if file == nil { + _ = unix.Close(fd) + return nil, errors.New("open plugin source returned an invalid file") + } + return file, nil +} + +func createPluginTarget(path string, mode os.FileMode) (*os.File, error) { + fd, err := unix.Open(path, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC|unix.O_NOFOLLOW, uint32(mode.Perm())) + if err != nil { + return nil, err + } + file := os.NewFile(uintptr(fd), path) + if file == nil { + _ = unix.Close(fd) + return nil, errors.New("create plugin target returned an invalid file") + } + return file, nil +} + func pluginIdentity(name string) (int, error) { value := os.Getenv(name) if value == "" { @@ -171,13 +201,13 @@ func pluginIdentity(name string) (int, error) { } func copyPluginExecutable(sourcePath, targetPath string, uid, gid int) error { - source, err := os.Open(sourcePath) + source, err := openPluginSource(sourcePath) if err != nil { return fmt.Errorf("open plugin executable: %w", err) } defer source.Close() - target, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700) + target, err := createPluginTarget(targetPath, 0700) if err != nil { return fmt.Errorf("create sandbox plugin executable: %w", err) } @@ -192,10 +222,10 @@ func copyPluginExecutable(sourcePath, targetPath string, uid, gid int) error { if err := target.Close(); err != nil { return fmt.Errorf("close sandbox plugin executable: %w", err) } - if err := os.Chmod(targetPath, 0700); err != nil { + if err := target.Chmod(0700); err != nil { return fmt.Errorf("secure sandbox plugin executable: %w", err) } - if err := os.Chown(targetPath, uid, gid); err != nil { + if err := target.Chown(uid, gid); err != nil { return fmt.Errorf("assign sandbox plugin identity: %w", err) } return nil