修复只读证书同步并拒绝硬链接私钥
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-20 04:53:18 +08:00
parent 951fdc00c4
commit 1790e1f4f6
6 changed files with 38 additions and 8 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -2,6 +2,12 @@
package config
import "os"
func hasMultipleInfoLinks(info os.FileInfo) bool {
return false
}
func hasMultipleFileLinks(path string) bool {
return false
}

View File

@ -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