fix: round 3 audit fixes - SMTP header sanitization and goroutine safety
- Move sanitizeEmailHeader to SendEmailWithConfig entry point, covering all email senders (verify code, password reset, ops alerts, notifications) - Add panic recovery to UpdateBalance goroutine - Fix stale comment in getAccountQuotaNotifyEmails (email="" no longer used) - Log error instead of silently discarding verifyNotifyCode cache update failure
This commit is contained in:
parent
b7fb2e4387
commit
b1875f0b82
@ -1 +1 @@
|
|||||||
0.1.110.20
|
0.1.110.21
|
||||||
|
|||||||
@ -225,7 +225,7 @@ func (s *BalanceNotifyService) isAccountQuotaNotifyEnabled(ctx context.Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getAccountQuotaNotifyEmails reads admin notification emails from settings,
|
// getAccountQuotaNotifyEmails reads admin notification emails from settings,
|
||||||
// filtering out disabled entries. Entries with email="" are resolved to the first admin's email.
|
// filtering out disabled and unverified entries.
|
||||||
func (s *BalanceNotifyService) getAccountQuotaNotifyEmails(ctx context.Context) []string {
|
func (s *BalanceNotifyService) getAccountQuotaNotifyEmails(ctx context.Context) []string {
|
||||||
raw, err := s.settingRepo.GetValue(ctx, SettingKeyAccountQuotaNotifyEmails)
|
raw, err := s.settingRepo.GetValue(ctx, SettingKeyAccountQuotaNotifyEmails)
|
||||||
if err != nil || strings.TrimSpace(raw) == "" || raw == "[]" {
|
if err != nil || strings.TrimSpace(raw) == "" || raw == "[]" {
|
||||||
|
|||||||
@ -153,9 +153,13 @@ func (s *EmailService) SendEmail(ctx context.Context, to, subject, body string)
|
|||||||
|
|
||||||
// SendEmailWithConfig 使用指定配置发送邮件
|
// SendEmailWithConfig 使用指定配置发送邮件
|
||||||
func (s *EmailService) SendEmailWithConfig(config *SMTPConfig, to, subject, body string) error {
|
func (s *EmailService) SendEmailWithConfig(config *SMTPConfig, to, subject, body string) error {
|
||||||
from := config.From
|
// Sanitize all SMTP header fields to prevent header injection (CR/LF removal).
|
||||||
|
to = sanitizeEmailHeader(to)
|
||||||
|
subject = sanitizeEmailHeader(subject)
|
||||||
|
|
||||||
|
from := sanitizeEmailHeader(config.From)
|
||||||
if config.FromName != "" {
|
if config.FromName != "" {
|
||||||
from = fmt.Sprintf("%s <%s>", config.FromName, config.From)
|
from = fmt.Sprintf("%s <%s>", sanitizeEmailHeader(config.FromName), sanitizeEmailHeader(config.From))
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s",
|
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s",
|
||||||
|
|||||||
@ -224,6 +224,11 @@ func (s *UserService) UpdateBalance(ctx context.Context, userID int64, amount fl
|
|||||||
}
|
}
|
||||||
if s.billingCache != nil {
|
if s.billingCache != nil {
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
slog.Error("panic in balance cache invalidation", "user_id", userID, "recover", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := s.billingCache.InvalidateUserBalance(cacheCtx, userID); err != nil {
|
if err := s.billingCache.InvalidateUserBalance(cacheCtx, userID); err != nil {
|
||||||
@ -359,7 +364,9 @@ func verifyNotifyCode(ctx context.Context, cache EmailCache, email, code string)
|
|||||||
}
|
}
|
||||||
if subtle.ConstantTimeCompare([]byte(data.Code), []byte(code)) != 1 {
|
if subtle.ConstantTimeCompare([]byte(data.Code), []byte(code)) != 1 {
|
||||||
data.Attempts++
|
data.Attempts++
|
||||||
_ = cache.SetNotifyVerifyCode(ctx, email, data, verifyCodeTTL)
|
if err := cache.SetNotifyVerifyCode(ctx, email, data, verifyCodeTTL); err != nil {
|
||||||
|
slog.Error("failed to update notify verify code attempts", "email", email, "error", err)
|
||||||
|
}
|
||||||
if data.Attempts >= maxVerifyCodeAttempts {
|
if data.Attempts >= maxVerifyCodeAttempts {
|
||||||
return ErrVerifyCodeMaxAttempts
|
return ErrVerifyCodeMaxAttempts
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user