This commit is contained in:
parent
d5d1fa27d1
commit
ed949f0ee8
@ -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验证记录,请稍候……",
|
||||
|
||||
@ -324,12 +324,12 @@
|
||||
|
||||
|
||||
<el-form-item :label="lang.ssl_key_path" v-if="sslSettings.type === '1'">
|
||||
<el-input placeholder="/ssl/example.com/example.com.key" v-model="sslSettings.key_path"></el-input>
|
||||
<el-input placeholder="/etc/letsencrypt/live/example.com/privkey.pem" v-model="sslSettings.key_path"></el-input>
|
||||
<div class="ssl-manual-desc">{{ lang.ssl_manual_key_desc }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="lang.ssl_crt_path" v-if="sslSettings.type === '1'">
|
||||
<el-input placeholder="/ssl/example.com/fullchain.pem" v-model="sslSettings.crt_path"></el-input>
|
||||
<el-input placeholder="/etc/letsencrypt/live/example.com/fullchain.pem" v-model="sslSettings.crt_path"></el-input>
|
||||
<div class="ssl-manual-desc">{{ lang.ssl_manual_crt_desc }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user