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