fix(security): 修复 P2 中危项(cookie/协议限速/路径遍历/默认口令/中继TLS/安全头/信息泄露)
- 会话 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。
This commit is contained in:
26 files changed
+799
-101
No files matched your search
@@ -48,6 +48,13 @@ func AuthMiddleware(stores *store.Stores) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 首次登录/密码被重置的用户必须先修改密码才能使用其他功能
|
||||
if user.MustChangePassword && c.Request.URL.Path != "/settings" && c.Request.URL.Path != "/logout" {
|
||||
c.Redirect(302, "/settings?force=1")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("currentUser", user)
|
||||
c.Set("userID", id)
|
||||
c.Next()
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// securityCSP 是本应用的基础 CSP。
|
||||
//
|
||||
// 说明:
|
||||
// - 模板大量使用内联脚本/样式(Quill 初始化、行内事件处理、
|
||||
// avatarStyle 内联 CSS),故 script-src / style-src 需要
|
||||
// 'unsafe-inline';
|
||||
// - 邮件正文在 srcdoc iframe 中渲染,可能引用远程图片(https:),
|
||||
// 因此 img-src 放行 https,同时仍阻止 data: 以外的自定义协议;
|
||||
// - frame-ancestors 'none' 与 X-Frame-Options 共同防护点击劫持;
|
||||
// - connect-src 'self' / form-action 'self' 阻止页面数据外泄到
|
||||
// 外部域名。
|
||||
const securityCSP = "default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline'; " +
|
||||
"style-src 'self' 'unsafe-inline'; " +
|
||||
"img-src 'self' data: https:; " +
|
||||
"connect-src 'self'; object-src 'none'; base-uri 'self'; " +
|
||||
"form-action 'self'; frame-ancestors 'none'"
|
||||
|
||||
// SecurityHeaders 为所有响应设置基础安全头:HSTS、点击劫持防护、
|
||||
// MIME 嗅探防护、Referrer 策略与基础 CSP。
|
||||
func SecurityHeaders() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
c.Header("X-Frame-Options", "DENY")
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
c.Header("Content-Security-Policy", securityCSP)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TestSecurityHeaders 验证所有基础安全响应头都存在。
|
||||
func TestSecurityHeaders(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(SecurityHeaders())
|
||||
r.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "ok") })
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
for _, h := range []string{
|
||||
"Strict-Transport-Security",
|
||||
"X-Frame-Options",
|
||||
"X-Content-Type-Options",
|
||||
"Referrer-Policy",
|
||||
"Content-Security-Policy",
|
||||
} {
|
||||
if v := w.Header().Get(h); v == "" {
|
||||
t.Errorf("missing security header %q", h)
|
||||
}
|
||||
}
|
||||
|
||||
// 关键头内容抽查
|
||||
if got := w.Header().Get("X-Frame-Options"); got != "DENY" {
|
||||
t.Errorf("X-Frame-Options = %q, want DENY", got)
|
||||
}
|
||||
if got := w.Header().Get("Content-Security-Policy"); !strings.Contains(got, "frame-ancestors 'none'") {
|
||||
t.Errorf("CSP should include frame-ancestors 'none', got %q", got)
|
||||
}
|
||||
if got := w.Header().Get("Content-Security-Policy"); !strings.Contains(got, "connect-src 'self'") {
|
||||
t.Errorf("CSP should include connect-src 'self', got %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user