fix(security): 修复密码重置链接 Host Header 注入漏洞 (P0-07)

ForgotPassword 原来从 c.Request.Host 构建重置链接基础 URL,攻击者
可伪造 Host 头将重置链接指向恶意域名窃取 token。

修复方案:
- ServerConfig 新增 frontend_url 配置项
- auth_handler 改为从配置读取前端 URL,未配置时拒绝请求
- Validate() 校验 frontend_url 必须为绝对 HTTP(S) URL
- 新增 TestValidateServerFrontendURL 单元测试
- config.example.yaml 添加配置说明

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yangjianbo
2026-02-07 17:15:26 +08:00
parent 8226a4ce4d
commit e75d3e3584
4 changed files with 38 additions and 10 deletions

View File

@@ -2,6 +2,7 @@ package handler
import (
"log/slog"
"strings"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
@@ -448,17 +449,12 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
return
}
// Build frontend base URL from request
scheme := "https"
if c.Request.TLS == nil {
// Check X-Forwarded-Proto header (common in reverse proxy setups)
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
scheme = proto
} else {
scheme = "http"
}
frontendBaseURL := strings.TrimSpace(h.cfg.Server.FrontendURL)
if frontendBaseURL == "" {
slog.Error("server.frontend_url not configured; cannot build password reset link")
response.InternalError(c, "Password reset is not configured")
return
}
frontendBaseURL := scheme + "://" + c.Request.Host
// Request password reset (async)
// Note: This returns success even if email doesn't exist (to prevent enumeration)