- 会话 cookie 增加 Secure 标志;新增 [web].cookie_secure 配置 (默认 true,仅本地 HTTP 调试关闭;缺失字段按安全默认处理) - SMTP/IMAP/POP3 认证接入封禁体系(store.RecordAuthFailure 与 Web 共用 ban_entries):失败计数达 ban.max_fail_attempts 即封禁 IP, 已封禁 IP 拒绝认证,堵住协议层暴力破解 - 附件存储路径遍历防护重写:FullPath 白名单校验(UUID 文件名格式) + baseDir 前缀兜底,非法路径返回错误;Save 扩展名白名单化 - 初始管理员不再使用 admin/admin:密码取 MAILGO_ADMIN_PASSWORD 或 随机生成并打印一次;新增 MustChangePassword 首登强制改密 (管理员重置密码同样触发) - 外发中继默认验证 TLS 证书(保护 AUTH 凭据,防 MITM),直投 MX 保持机会式 TLS;新增 outbound.relay_tls_insecure 开关(默认 false) - 新增安全响应头中间件:HSTS、X-Frame-Options DENY、nosniff、 Referrer-Policy、基础 CSP(frame-ancestors 'none' 防点击劫持, connect-src/form-action 'self' 防数据外泄) - LDAP/OAuth 登录错误统一为通用文案,原始错误只写日志, 不再回显邮箱/内部细节(防用户枚举与信息泄露) - 新增 25 个回归测试:cookie 标志、封禁阈值、路径遍历用例、 中继 TLS 验证(自签证书 STARTTLS 集成)、安全头、OAuth 文案 部署注意:升级后所有会话失效需重新登录;若直接以 HTTP 提供 服务需显式配置 cookie_secure = false。
98 lines
2.5 KiB
Go
98 lines
2.5 KiB
Go
package storage
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TestFullPathRejectsTraversal 验证路径遍历/绝对路径等恶意输入被拒绝。
|
|
func TestFullPathRejectsTraversal(t *testing.T) {
|
|
s := NewAttachmentStorage(filepath.Join(t.TempDir(), "attachments"))
|
|
|
|
valid := uuid.New().String() + ".pdf"
|
|
bad := []string{
|
|
"../secret.txt",
|
|
"../../etc/passwd",
|
|
"..",
|
|
"....//x",
|
|
"/etc/passwd",
|
|
"a/../b.txt",
|
|
"sub/file.png",
|
|
"",
|
|
".", "..\\..\\x", // windows style
|
|
"00000000-0000-0000-0000-000000000000.exe\r\nBcc: x@y.com",
|
|
"garbage",
|
|
"00000000-0000-0000-0000-000000000000.%2e%2e",
|
|
}
|
|
for _, p := range bad {
|
|
if _, err := s.FullPath(p); err == nil {
|
|
t.Errorf("FullPath(%q) should be rejected", p)
|
|
}
|
|
}
|
|
|
|
// 合法文件名必须通过
|
|
full, err := s.FullPath(valid)
|
|
if err != nil {
|
|
t.Fatalf("FullPath(%q) rejected: %v", valid, err)
|
|
}
|
|
if !strings.HasPrefix(full, s.baseDir+string(os.PathSeparator)) {
|
|
t.Fatalf("FullPath(%q) = %q escapes baseDir", valid, full)
|
|
}
|
|
}
|
|
|
|
// TestSaveSanitizesExtension 验证恶意扩展名不会进入文件名。
|
|
func TestSaveSanitizesExtension(t *testing.T) {
|
|
s := NewAttachmentStorage(filepath.Join(t.TempDir(), "attachments"))
|
|
|
|
// 换行/路径分隔符等非法字符的扩展名应被丢弃
|
|
rel, err := s.Save("evil.pdf\r\nBcc: x@y.com", []byte("data"))
|
|
if err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
if strings.ContainsAny(rel, "\r\n/\\") {
|
|
t.Fatalf("saved name contains dangerous chars: %q", rel)
|
|
}
|
|
if !savedFileRe.MatchString(rel) {
|
|
t.Fatalf("saved name %q does not match allowed pattern", rel)
|
|
}
|
|
// 后续 Read 应能按返回的路径读取
|
|
if _, err := s.Read(rel); err != nil {
|
|
t.Fatalf("Read after Save: %v", err)
|
|
}
|
|
|
|
// 正常扩展名保留
|
|
rel2, err := s.Save("report.pdf", []byte("data"))
|
|
if err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
if !strings.HasSuffix(rel2, ".pdf") {
|
|
t.Fatalf("extension lost: %q", rel2)
|
|
}
|
|
}
|
|
|
|
// TestReadDeleteRoundTrip 正常读写删流程。
|
|
func TestReadDeleteRoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s := NewAttachmentStorage(filepath.Join(dir, "attachments"))
|
|
|
|
rel, err := s.Save("a.txt", []byte("hello"))
|
|
if err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
data, err := s.Read(rel)
|
|
if err != nil || string(data) != "hello" {
|
|
t.Fatalf("Read = %q, %v", data, err)
|
|
}
|
|
if err := s.Delete(rel); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
// 删除后路径仍然合法(删除不存在文件不算错误)
|
|
if err := s.Delete(rel); err != nil {
|
|
t.Fatalf("Delete again: %v", err)
|
|
}
|
|
}
|