From 1790e1f4f61d3bddc77a63f5f21339202a12f452 Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 20 Sep 2026 04:53:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=AA=E8=AF=BB=E8=AF=81?= =?UTF-8?q?=E4=B9=A6=E5=90=8C=E6=AD=A5=E5=B9=B6=E6=8B=92=E7=BB=9D=E7=A1=AC?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E7=A7=81=E9=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/manager.go | 6 ++++-- backend/config/ssl_export.go | 12 ++++++++---- backend/config/ssl_export_test.go | 10 ++++++++++ backend/config/ssl_storage_links_unix.go | 10 +++++++++- backend/config/ssl_storage_links_windows.go | 6 ++++++ deploy/systemd/smail-tls-sync.service | 2 +- 6 files changed, 38 insertions(+), 8 deletions(-) diff --git a/backend/config/manager.go b/backend/config/manager.go index 9d36e73..441c6ec 100644 --- a/backend/config/manager.go +++ b/backend/config/manager.go @@ -329,8 +329,10 @@ func secureManagerConfigDirectory(root string) (string, error) { } // 目录权限 0700 是配置目录的最低要求;G302 将目录误判为文件。 // #nosec G302 - if err := os.Chmod(directory, 0700); err != nil { - return "", err + if info.Mode().Perm() != 0700 { + if err := os.Chmod(directory, 0700); err != nil { + return "", err + } } return directory, nil } diff --git a/backend/config/ssl_export.go b/backend/config/ssl_export.go index 57b905e..385c16a 100644 --- a/backend/config/ssl_export.go +++ b/backend/config/ssl_export.go @@ -38,6 +38,10 @@ func ExportProjectTLS(root, destination string) error { if err != nil || resolvedSource != sourceDirectory { return ErrUnsafeSSLFile } + sourceInfo, err := os.Stat(sourceDirectory) + if err != nil || !sourceInfo.IsDir() || sourceInfo.Mode().Perm()&0022 != 0 { + return ErrUnsafeSSLFile + } storage, err := os.OpenRoot(sourceDirectory) if err != nil { return ErrUnsafeSSLFile @@ -45,7 +49,7 @@ func ExportProjectTLS(root, destination string) error { defer storage.Close() read := func(name string) ([]byte, error) { info, err := storage.Lstat(name) - if err != nil || !info.Mode().IsRegular() || info.Size() > maxManagedSSLFileSize || info.Mode().Perm()&0077 != 0 { + if err != nil || !info.Mode().IsRegular() || hasMultipleInfoLinks(info) || info.Size() > maxManagedSSLFileSize || info.Mode().Perm()&0077 != 0 { return nil, ErrUnsafeSSLFile } file, err := storage.Open(name) @@ -54,7 +58,7 @@ func ExportProjectTLS(root, destination string) error { } defer file.Close() opened, err := file.Stat() - if err != nil || !os.SameFile(info, opened) || !opened.Mode().IsRegular() || opened.Mode().Perm()&0077 != 0 { + if err != nil || !os.SameFile(info, opened) || !opened.Mode().IsRegular() || hasMultipleInfoLinks(opened) || opened.Mode().Perm()&0077 != 0 { return nil, ErrUnsafeSSLFile } data, err := io.ReadAll(io.LimitReader(file, maxManagedSSLFileSize+1)) @@ -62,7 +66,7 @@ func ExportProjectTLS(root, destination string) error { return nil, ErrUnsafeSSLFile } after, err := file.Stat() - if err != nil || !os.SameFile(opened, after) || after.Size() != int64(len(data)) || !opened.ModTime().Equal(after.ModTime()) || after.Mode().Perm()&0077 != 0 { + if err != nil || !os.SameFile(opened, after) || hasMultipleInfoLinks(after) || after.Size() != int64(len(data)) || !opened.ModTime().Equal(after.ModTime()) || after.Mode().Perm()&0077 != 0 { return nil, ErrUnsafeSSLFile } return data, nil @@ -96,7 +100,7 @@ func ExportProjectTLS(root, destination string) error { defer output.Close() name := filepath.Base(destination) if info, err := output.Lstat(name); err == nil { - if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 { + if !info.Mode().IsRegular() || hasMultipleInfoLinks(info) || info.Mode().Perm()&0077 != 0 { return ErrUnsafeSSLFile } previous, err := output.ReadFile(name) diff --git a/backend/config/ssl_export_test.go b/backend/config/ssl_export_test.go index f4aebf1..26418d9 100644 --- a/backend/config/ssl_export_test.go +++ b/backend/config/ssl_export_test.go @@ -81,6 +81,16 @@ func TestExportProjectTLSKeepsOldBundleOnInvalidInput(t *testing.T) { if err := ExportProjectTLS(root, filepath.Join(link, "tls.pem")); err == nil { t.Fatal("linked output directory accepted") } + hardLink := filepath.Join(t.TempDir(), "linked.key") + if err := os.Link(ref.PrivateKeyPath, hardLink); err != nil { + t.Fatal(err) + } + if err := ExportProjectTLS(root, destination); err == nil { + t.Fatal("hard-linked private key accepted") + } + if err := os.Remove(hardLink); err != nil { + t.Fatal(err) + } if err := os.WriteFile(ref.PrivateKeyPath, []byte("invalid"), 0600); err != nil { t.Fatal(err) } diff --git a/backend/config/ssl_storage_links_unix.go b/backend/config/ssl_storage_links_unix.go index b328875..5bd2398 100644 --- a/backend/config/ssl_storage_links_unix.go +++ b/backend/config/ssl_storage_links_unix.go @@ -2,7 +2,15 @@ package config -import "syscall" +import ( + "os" + "syscall" +) + +func hasMultipleInfoLinks(info os.FileInfo) bool { + stat, ok := info.Sys().(*syscall.Stat_t) + return !ok || stat.Nlink != 1 +} func hasMultipleFileLinks(path string) bool { var stat syscall.Stat_t diff --git a/backend/config/ssl_storage_links_windows.go b/backend/config/ssl_storage_links_windows.go index 6bcf807..d2e4cb6 100644 --- a/backend/config/ssl_storage_links_windows.go +++ b/backend/config/ssl_storage_links_windows.go @@ -2,6 +2,12 @@ package config +import "os" + +func hasMultipleInfoLinks(info os.FileInfo) bool { + return false +} + func hasMultipleFileLinks(path string) bool { return false } diff --git a/deploy/systemd/smail-tls-sync.service b/deploy/systemd/smail-tls-sync.service index 149c541..fb24df8 100644 --- a/deploy/systemd/smail-tls-sync.service +++ b/deploy/systemd/smail-tls-sync.service @@ -15,7 +15,7 @@ NoNewPrivileges=true PrivateTmp=true ProtectHome=true ProtectSystem=strict -ReadWritePaths=/etc/nginx/smail /opt/smail/data /var/log/nginx +ReadWritePaths=/etc/nginx/smail /var/log/nginx TimeoutStartSec=30 Restart=on-failure RestartSec=5