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_certificate_path": "证书:./config/ssl/managed/default/current/public.crt",
|
||||||
"ssl_auto_private_key_path": "私钥:./config/ssl/managed/default/current/private.key",
|
"ssl_auto_private_key_path": "私钥:./config/ssl/managed/default/current/private.key",
|
||||||
"ssl_auto_account_key_path": "ACME 账户密钥:./config/ssl/account_private.pem",
|
"ssl_auto_account_key_path": "ACME 账户密钥:./config/ssl/account_private.pem",
|
||||||
"ssl_manual_key_desc": "填写 ./config/ssl 下的相对路径,或 /ssl/域名 下的绝对路径;系统校验后会导入受控目录。",
|
"ssl_manual_key_desc": "可填写服务器上任意可读取的完整路径,也可填写相对于 Smail 目录的路径;系统校验后会导入受控目录。",
|
||||||
"ssl_manual_crt_desc": "填写 ./config/ssl 下的相对路径,或 /ssl/域名 下的绝对路径;证书必须与私钥匹配。",
|
"ssl_manual_crt_desc": "可填写服务器上任意可读取的完整路径,也可填写相对于 Smail 目录的路径;证书必须与私钥匹配。",
|
||||||
"ssl_dns_title": "DNS验证记录",
|
"ssl_dns_title": "DNS验证记录",
|
||||||
"ssl_dns_desc": "选择DNS验证后,点击“下一步”生成本次申请所需的TXT记录。添加记录并等待DNS生效后,再继续验证。",
|
"ssl_dns_desc": "选择DNS验证后,点击“下一步”生成本次申请所需的TXT记录。添加记录并等待DNS生效后,再继续验证。",
|
||||||
"ssl_dns_waiting": "正在生成DNS验证记录,请稍候……",
|
"ssl_dns_waiting": "正在生成DNS验证记录,请稍候……",
|
||||||
|
|||||||
@ -324,12 +324,12 @@
|
|||||||
|
|
||||||
|
|
||||||
<el-form-item :label="lang.ssl_key_path" v-if="sslSettings.type === '1'">
|
<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>
|
<div class="ssl-manual-desc">{{ lang.ssl_manual_key_desc }}</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item :label="lang.ssl_crt_path" v-if="sslSettings.type === '1'">
|
<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>
|
<div class="ssl-manual-desc">{{ lang.ssl_manual_crt_desc }}</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|||||||
@ -33,8 +33,6 @@ type MyUser struct {
|
|||||||
key crypto.PrivateKey
|
key crypto.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
var externalUserCertificateRoot = filepath.Join(string(os.PathSeparator), "ssl")
|
|
||||||
|
|
||||||
type userCertificateError struct {
|
type userCertificateError struct {
|
||||||
message string
|
message string
|
||||||
}
|
}
|
||||||
@ -169,23 +167,15 @@ func validateUserCertificatePaths(privatePath, publicPath string) (string, strin
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
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) {
|
resolve := func(value string) (string, []byte, error) {
|
||||||
if strings.TrimSpace(value) == "" {
|
if strings.TrimSpace(value) == "" {
|
||||||
return "", nil, newUserCertificateError("请填写 SSL 证书和私钥路径")
|
return "", nil, newUserCertificateError("请填写 SSL 证书和私钥路径")
|
||||||
}
|
}
|
||||||
path := strings.TrimSpace(value)
|
path := strings.TrimSpace(value)
|
||||||
allowedRoots := []string{managedRoot}
|
|
||||||
if isAbsoluteUserCertificatePath(path) {
|
if isAbsoluteUserCertificatePath(path) {
|
||||||
if filepath.VolumeName(path) != "" && !filepath.IsAbs(path) {
|
if filepath.VolumeName(path) != "" && !filepath.IsAbs(path) {
|
||||||
return "", nil, newUserCertificateError("SSL 路径必须是完整的绝对路径")
|
return "", nil, newUserCertificateError("SSL 路径必须是完整的绝对路径")
|
||||||
}
|
}
|
||||||
allowedRoots = append(allowedRoots, externalRoot)
|
|
||||||
} else {
|
} else {
|
||||||
path = filepath.Join(rootPath, path)
|
path = filepath.Join(rootPath, path)
|
||||||
}
|
}
|
||||||
@ -194,58 +184,18 @@ func validateUserCertificatePaths(privatePath, publicPath string) (string, strin
|
|||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedRoot string
|
info, err := os.Stat(path)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, newUserCertificateError("SSL 文件不存在或无权读取")
|
return "", nil, newUserCertificateError("SSL 文件不存在或无权读取")
|
||||||
}
|
}
|
||||||
if !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > 1<<20 {
|
if !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > 1<<20 {
|
||||||
return "", nil, newUserCertificateError("SSL 文件无效或大小超过限制")
|
return "", nil, newUserCertificateError("SSL 文件无效或大小超过限制")
|
||||||
}
|
}
|
||||||
content, err := os.ReadFile(candidatePath)
|
content, err := os.ReadFile(path)
|
||||||
if err != nil || len(content) == 0 || len(content) > 1<<20 {
|
if err != nil || len(content) == 0 || len(content) > 1<<20 {
|
||||||
return "", nil, newUserCertificateError("SSL 文件读取失败或内容无效")
|
return "", nil, newUserCertificateError("SSL 文件读取失败或内容无效")
|
||||||
}
|
}
|
||||||
return filepath.Join(selectedRoot, relativePath), content, nil
|
return path, content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
privatePath, privatePEM, err := resolve(privatePath)
|
privatePath, privatePEM, err := resolve(privatePath)
|
||||||
|
|||||||
@ -49,7 +49,7 @@ func writeTestCertificate(t *testing.T, root string, bits int) (string, string)
|
|||||||
return keyPath, certPath
|
return keyPath, certPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsAbsoluteUserCertificatePathRejectsWindowsPaths(t *testing.T) {
|
func TestIsAbsoluteUserCertificatePathRecognizesWindowsPaths(t *testing.T) {
|
||||||
cases := []string{
|
cases := []string{
|
||||||
`C:\outside\private.key`,
|
`C:\outside\private.key`,
|
||||||
`D:/outside/public.crt`,
|
`D:/outside/public.crt`,
|
||||||
@ -100,14 +100,10 @@ func TestApplyManagedSSLReferenceDoesNotPersistLegacyPaths(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateUserCertificatePaths(t *testing.T) {
|
func TestValidateUserCertificatePaths(t *testing.T) {
|
||||||
oldRoot := config.ROOT_PATH
|
oldRoot := config.ROOT_PATH
|
||||||
oldExternalRoot := externalUserCertificateRoot
|
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
externalRoot := t.TempDir()
|
|
||||||
config.ROOT_PATH = root + string(os.PathSeparator)
|
config.ROOT_PATH = root + string(os.PathSeparator)
|
||||||
externalUserCertificateRoot = externalRoot
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
config.ROOT_PATH = oldRoot
|
config.ROOT_PATH = oldRoot
|
||||||
externalUserCertificateRoot = oldExternalRoot
|
|
||||||
})
|
})
|
||||||
|
|
||||||
keyPath, certPath := writeTestCertificate(t, root, 2048)
|
keyPath, certPath := writeTestCertificate(t, root, 2048)
|
||||||
@ -116,43 +112,34 @@ func TestValidateUserCertificatePaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
externalSourceRoot := t.TempDir()
|
externalSourceRoot := t.TempDir()
|
||||||
externalKey, externalCert := writeTestCertificate(t, externalSourceRoot, 2048)
|
externalKey, externalCert := writeTestCertificate(t, externalSourceRoot, 2048)
|
||||||
externalDirectory := filepath.Join(externalRoot, "example.com")
|
if _, _, err := validateUserCertificatePaths(externalKey, externalCert); err != nil {
|
||||||
if err := os.MkdirAll(externalDirectory, 0700); err != nil {
|
t.Fatalf("certificate in an arbitrary directory was rejected: %v", err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
externalKeyTarget := filepath.Join(externalDirectory, "private.key")
|
relativeKey, err := filepath.Rel(root, externalKey)
|
||||||
externalCertTarget := filepath.Join(externalDirectory, "public.crt")
|
|
||||||
keyPEM, err := os.ReadFile(externalKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
certPEM, err := os.ReadFile(externalCert)
|
relativeCert, err := filepath.Rel(root, externalCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(externalKeyTarget, keyPEM, 0640); err != nil {
|
if _, _, err := validateUserCertificatePaths(relativeKey, relativeCert); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatalf("certificate in an arbitrary relative directory was rejected: %v", 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)
|
|
||||||
}
|
}
|
||||||
weakKey, weakCert := writeTestCertificate(t, root, 1024)
|
weakKey, weakCert := writeTestCertificate(t, root, 1024)
|
||||||
if _, _, err := validateUserCertificatePaths(weakKey, weakCert); err == nil {
|
if _, _, err := validateUserCertificatePaths(weakKey, weakCert); err == nil {
|
||||||
t.Fatal("weak certificate was accepted")
|
t.Fatal("weak certificate was accepted")
|
||||||
}
|
}
|
||||||
outside := filepath.Join(t.TempDir(), "outside.key")
|
invalid := filepath.Join(t.TempDir(), "invalid.key")
|
||||||
if err := os.WriteFile(outside, []byte("invalid"), 0600); err != nil {
|
if err := os.WriteFile(invalid, []byte("invalid"), 0600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, _, err := validateUserCertificatePaths(outside, certPath); err == nil {
|
if _, _, err := validateUserCertificatePaths(invalid, certPath); err == nil {
|
||||||
t.Fatal("outside path was accepted")
|
t.Fatal("invalid certificate data was accepted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateUserCertificatePathsRejectsEscapingParentSymlink(t *testing.T) {
|
func TestValidateUserCertificatePathsAcceptsParentSymlink(t *testing.T) {
|
||||||
oldRoot := config.ROOT_PATH
|
oldRoot := config.ROOT_PATH
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
config.ROOT_PATH = root + string(os.PathSeparator)
|
config.ROOT_PATH = root + string(os.PathSeparator)
|
||||||
@ -170,7 +157,7 @@ func TestValidateUserCertificatePathsRejectsEscapingParentSymlink(t *testing.T)
|
|||||||
}
|
}
|
||||||
linkedKey := filepath.Join(linkPath, filepath.Base(outsideKey))
|
linkedKey := filepath.Join(linkPath, filepath.Base(outsideKey))
|
||||||
linkedCert := filepath.Join(linkPath, filepath.Base(outsideCert))
|
linkedCert := filepath.Join(linkPath, filepath.Base(outsideCert))
|
||||||
if _, _, err := validateUserCertificatePaths(linkedKey, linkedCert); err == nil {
|
if _, _, err := validateUserCertificatePaths(linkedKey, linkedCert); err != nil {
|
||||||
t.Fatal("certificate paths escaped through a parent symlink")
|
t.Fatalf("certificate paths through a parent symlink were rejected: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user