Potential fix for code scanning alert no. 1271: Email content injection
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"mime"
|
||||
"net/mail"
|
||||
@@ -329,6 +330,9 @@ func (s *MailService) SendEmail(ctx context.Context, to []string, subject, htmlB
|
||||
auth = smtp.PlainAuth("", config.Username, config.Password, config.Host)
|
||||
}
|
||||
|
||||
// Normalize and sanitize the email body so that any untrusted input is
|
||||
// treated as plain text and cannot break out of the HTML context.
|
||||
htmlBody = sanitizeAndNormalizeHTMLBody(htmlBody)
|
||||
htmlBody = sanitizeEmailContent(htmlBody)
|
||||
|
||||
for _, recipient := range to {
|
||||
@@ -494,6 +498,31 @@ func sanitizeEmailContent(body string) string {
|
||||
}, body)
|
||||
}
|
||||
|
||||
// sanitizeAndNormalizeHTMLBody converts an arbitrary string (potentially containing
|
||||
// untrusted input) into a safe HTML fragment. It splits on newlines, escapes each
|
||||
// line as plain text, and wraps non-empty lines in <p> tags. This ensures that
|
||||
// user input cannot inject raw HTML into the email body.
|
||||
func sanitizeAndNormalizeHTMLBody(body string) string {
|
||||
if body == "" {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(body, "\n")
|
||||
var b strings.Builder
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if b.Len() > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("<p>")
|
||||
b.WriteString(html.EscapeString(line))
|
||||
b.WriteString("</p>")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// sanitizeEmailBody performs SMTP dot-stuffing to prevent email injection.
|
||||
// According to RFC 5321, if a line starts with a period, it must be doubled
|
||||
// to prevent premature termination of the SMTP DATA command.
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"net"
|
||||
"net/http"
|
||||
neturl "net/url"
|
||||
@@ -284,12 +283,24 @@ func (s *NotificationService) dispatchEmail(ctx context.Context, p models.Notifi
|
||||
safeTitle := sanitizeForEmail(title)
|
||||
safeMessage := sanitizeForEmail(message)
|
||||
subject := fmt.Sprintf("[Charon Alert] %s", safeTitle)
|
||||
htmlBody := "<p><strong>" + html.EscapeString(safeTitle) + "</strong></p><p>" + html.EscapeString(safeMessage) + "</p>"
|
||||
// Build a plain-text body; MailService will convert this to safe HTML and
|
||||
// perform additional sanitization before sending.
|
||||
var bodyBuilder strings.Builder
|
||||
if safeTitle != "" {
|
||||
bodyBuilder.WriteString(safeTitle)
|
||||
}
|
||||
if safeMessage != "" {
|
||||
if bodyBuilder.Len() > 0 {
|
||||
bodyBuilder.WriteString("\n\n")
|
||||
}
|
||||
bodyBuilder.WriteString(safeMessage)
|
||||
}
|
||||
plainBody := bodyBuilder.String()
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.mailService.SendEmail(timeoutCtx, recipients, subject, htmlBody); err != nil {
|
||||
if err := s.mailService.SendEmail(timeoutCtx, recipients, subject, plainBody); err != nil {
|
||||
logger.Log().WithError(err).WithField("provider", util.SanitizeForLog(p.Name)).Error("Failed to send email notification")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user