From ed949f0ee807ffb7d7ca015a3381b6fdbf83db6c Mon Sep 17 00:00:00 2001 From: smail Date: Wed, 2 Sep 2026 16:23:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BB=BB=E6=84=8F=E6=89=8B?= =?UTF-8?q?=E5=8A=A8SSL=E8=AF=81=E4=B9=A6=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fe/src/i18n/i18n.js | 4 +- fe/src/views/Setup/Setup.vue | 4 +- server/services/setup/ssl/ssl.go | 56 +------------------ .../setup/ssl/user_certificate_test.go | 41 +++++--------- 4 files changed, 21 insertions(+), 84 deletions(-) diff --git a/fe/src/i18n/i18n.js b/fe/src/i18n/i18n.js index 88f3dff..c4334d1 100644 --- a/fe/src/i18n/i18n.js +++ b/fe/src/i18n/i18n.js @@ -134,8 +134,8 @@ const lang = { "ssl_auto_certificate_path": "证书:./config/ssl/managed/default/current/public.crt", "ssl_auto_private_key_path": "私钥:./config/ssl/managed/default/current/private.key", "ssl_auto_account_key_path": "ACME 账户密钥:./config/ssl/account_private.pem", - "ssl_manual_key_desc": "填写 ./config/ssl 下的相对路径,或 /ssl/域名 下的绝对路径;系统校验后会导入受控目录。", - "ssl_manual_crt_desc": "填写 ./config/ssl 下的相对路径,或 /ssl/域名 下的绝对路径;证书必须与私钥匹配。", + "ssl_manual_key_desc": "可填写服务器上任意可读取的完整路径,也可填写相对于 Smail 目录的路径;系统校验后会导入受控目录。", + "ssl_manual_crt_desc": "可填写服务器上任意可读取的完整路径,也可填写相对于 Smail 目录的路径;证书必须与私钥匹配。", "ssl_dns_title": "DNS验证记录", "ssl_dns_desc": "选择DNS验证后,点击“下一步”生成本次申请所需的TXT记录。添加记录并等待DNS生效后,再继续验证。", "ssl_dns_waiting": "正在生成DNS验证记录,请稍候……", diff --git a/fe/src/views/Setup/Setup.vue b/fe/src/views/Setup/Setup.vue index 6282a97..8ba49b3 100644 --- a/fe/src/views/Setup/Setup.vue +++ b/fe/src/views/Setup/Setup.vue @@ -324,12 +324,12 @@ - +
{{ lang.ssl_manual_key_desc }}
- +
{{ lang.ssl_manual_crt_desc }}
diff --git a/server/services/setup/ssl/ssl.go b/server/services/setup/ssl/ssl.go index 8daf1ff..3bc9ca0 100644 --- a/server/services/setup/ssl/ssl.go +++ b/server/services/setup/ssl/ssl.go @@ -33,8 +33,6 @@ type MyUser struct { key crypto.PrivateKey } -var externalUserCertificateRoot = filepath.Join(string(os.PathSeparator), "ssl") - type userCertificateError struct { message string } @@ -169,23 +167,15 @@ func validateUserCertificatePaths(privatePath, publicPath string) (string, strin if err != nil { return "", "", err } - managedRoot := filepath.Join(rootPath, "config", "ssl") - externalRoot, err := filepath.Abs(filepath.Clean(externalUserCertificateRoot)) - if err != nil { - return "", "", err - } - resolve := func(value string) (string, []byte, error) { if strings.TrimSpace(value) == "" { return "", nil, newUserCertificateError("请填写 SSL 证书和私钥路径") } path := strings.TrimSpace(value) - allowedRoots := []string{managedRoot} if isAbsoluteUserCertificatePath(path) { if filepath.VolumeName(path) != "" && !filepath.IsAbs(path) { return "", nil, newUserCertificateError("SSL 路径必须是完整的绝对路径") } - allowedRoots = append(allowedRoots, externalRoot) } else { path = filepath.Join(rootPath, path) } @@ -194,58 +184,18 @@ func validateUserCertificatePaths(privatePath, publicPath string) (string, strin return "", nil, err } - var selectedRoot string - var relativePath string - for _, allowedRoot := range allowedRoots { - candidate, relativeErr := filepath.Rel(allowedRoot, path) - if relativeErr == nil && candidate != "." && !filepath.IsAbs(candidate) && candidate != ".." && !strings.HasPrefix(candidate, ".."+string(os.PathSeparator)) { - selectedRoot = allowedRoot - relativePath = candidate - break - } - } - if selectedRoot == "" { - return "", nil, newUserCertificateError("SSL 文件仅允许放在 ./config/ssl 或 /ssl/域名 目录中") - } - if filepath.Clean(selectedRoot) == filepath.Clean(externalRoot) { - parts := strings.Split(filepath.Clean(relativePath), string(os.PathSeparator)) - if len(parts) < 2 || strings.TrimSpace(parts[0]) == "" { - return "", nil, newUserCertificateError("外部 SSL 文件必须放在 /ssl/域名 目录中") - } - selectedRoot = filepath.Join(externalRoot, parts[0]) - relativePath = filepath.Join(parts[1:]...) - } - - rootInfo, err := os.Lstat(selectedRoot) - if err != nil || rootInfo.Mode()&os.ModeSymlink != 0 || !rootInfo.IsDir() { - return "", nil, newUserCertificateError("SSL 证书目录不存在或不可访问") - } - - candidatePath := filepath.Join(selectedRoot, relativePath) - currentPath := selectedRoot - for _, part := range strings.Split(filepath.Clean(relativePath), string(os.PathSeparator)) { - currentPath = filepath.Join(currentPath, part) - info, statErr := os.Lstat(currentPath) - if statErr != nil { - return "", nil, newUserCertificateError("SSL 文件不存在或无权读取") - } - if info.Mode()&os.ModeSymlink != 0 { - return "", nil, newUserCertificateError("SSL 路径不能包含符号链接") - } - } - - info, err := os.Lstat(candidatePath) + info, err := os.Stat(path) if err != nil { return "", nil, newUserCertificateError("SSL 文件不存在或无权读取") } if !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > 1<<20 { return "", nil, newUserCertificateError("SSL 文件无效或大小超过限制") } - content, err := os.ReadFile(candidatePath) + content, err := os.ReadFile(path) if err != nil || len(content) == 0 || len(content) > 1<<20 { return "", nil, newUserCertificateError("SSL 文件读取失败或内容无效") } - return filepath.Join(selectedRoot, relativePath), content, nil + return path, content, nil } privatePath, privatePEM, err := resolve(privatePath) diff --git a/server/services/setup/ssl/user_certificate_test.go b/server/services/setup/ssl/user_certificate_test.go index 2e53784..e813db0 100644 --- a/server/services/setup/ssl/user_certificate_test.go +++ b/server/services/setup/ssl/user_certificate_test.go @@ -49,7 +49,7 @@ func writeTestCertificate(t *testing.T, root string, bits int) (string, string) return keyPath, certPath } -func TestIsAbsoluteUserCertificatePathRejectsWindowsPaths(t *testing.T) { +func TestIsAbsoluteUserCertificatePathRecognizesWindowsPaths(t *testing.T) { cases := []string{ `C:\outside\private.key`, `D:/outside/public.crt`, @@ -100,14 +100,10 @@ func TestApplyManagedSSLReferenceDoesNotPersistLegacyPaths(t *testing.T) { func TestValidateUserCertificatePaths(t *testing.T) { oldRoot := config.ROOT_PATH - oldExternalRoot := externalUserCertificateRoot root := t.TempDir() - externalRoot := t.TempDir() config.ROOT_PATH = root + string(os.PathSeparator) - externalUserCertificateRoot = externalRoot t.Cleanup(func() { config.ROOT_PATH = oldRoot - externalUserCertificateRoot = oldExternalRoot }) keyPath, certPath := writeTestCertificate(t, root, 2048) @@ -116,43 +112,34 @@ func TestValidateUserCertificatePaths(t *testing.T) { } externalSourceRoot := t.TempDir() externalKey, externalCert := writeTestCertificate(t, externalSourceRoot, 2048) - externalDirectory := filepath.Join(externalRoot, "example.com") - if err := os.MkdirAll(externalDirectory, 0700); err != nil { - t.Fatal(err) + if _, _, err := validateUserCertificatePaths(externalKey, externalCert); err != nil { + t.Fatalf("certificate in an arbitrary directory was rejected: %v", err) } - externalKeyTarget := filepath.Join(externalDirectory, "private.key") - externalCertTarget := filepath.Join(externalDirectory, "public.crt") - keyPEM, err := os.ReadFile(externalKey) + relativeKey, err := filepath.Rel(root, externalKey) if err != nil { t.Fatal(err) } - certPEM, err := os.ReadFile(externalCert) + relativeCert, err := filepath.Rel(root, externalCert) if err != nil { t.Fatal(err) } - if err := os.WriteFile(externalKeyTarget, keyPEM, 0640); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(externalCertTarget, certPEM, 0644); err != nil { - t.Fatal(err) - } - if _, _, err := validateUserCertificatePaths(externalKeyTarget, externalCertTarget); err != nil { - t.Fatalf("external /ssl certificate was rejected: %v", err) + if _, _, err := validateUserCertificatePaths(relativeKey, relativeCert); err != nil { + t.Fatalf("certificate in an arbitrary relative directory was rejected: %v", err) } weakKey, weakCert := writeTestCertificate(t, root, 1024) if _, _, err := validateUserCertificatePaths(weakKey, weakCert); err == nil { t.Fatal("weak certificate was accepted") } - outside := filepath.Join(t.TempDir(), "outside.key") - if err := os.WriteFile(outside, []byte("invalid"), 0600); err != nil { + invalid := filepath.Join(t.TempDir(), "invalid.key") + if err := os.WriteFile(invalid, []byte("invalid"), 0600); err != nil { t.Fatal(err) } - if _, _, err := validateUserCertificatePaths(outside, certPath); err == nil { - t.Fatal("outside path was accepted") + if _, _, err := validateUserCertificatePaths(invalid, certPath); err == nil { + t.Fatal("invalid certificate data was accepted") } } -func TestValidateUserCertificatePathsRejectsEscapingParentSymlink(t *testing.T) { +func TestValidateUserCertificatePathsAcceptsParentSymlink(t *testing.T) { oldRoot := config.ROOT_PATH root := t.TempDir() config.ROOT_PATH = root + string(os.PathSeparator) @@ -170,7 +157,7 @@ func TestValidateUserCertificatePathsRejectsEscapingParentSymlink(t *testing.T) } linkedKey := filepath.Join(linkPath, filepath.Base(outsideKey)) linkedCert := filepath.Join(linkPath, filepath.Base(outsideCert)) - if _, _, err := validateUserCertificatePaths(linkedKey, linkedCert); err == nil { - t.Fatal("certificate paths escaped through a parent symlink") + if _, _, err := validateUserCertificatePaths(linkedKey, linkedCert); err != nil { + t.Fatalf("certificate paths through a parent symlink were rejected: %v", err) } }