Repository navigation
fix(email): plain-text templates HTML-escape data + SMTP cleartext-credential guard (07-H1, 07-H2, 07-M1--M3, 07-L2--L3, 07-N2, 07-N4) #1077
Description
Problem
Two security/correctness gaps remain in the email package after PR #1036 addressed the critical C1 token-broadcast bug.
07-H1 (High): Plain-text email bodies are HTML-escaped, corrupting data
internal/email/template_renderers.go:6,16-28
renderTemplate uses html/template for all templates, including the plain-text halves. Any data field containing &, <, >, ', " is emitted as HTML entities in plain-text bodies (O'Brien -> O'Brien, a&b@x.com -> a&b@x.com). Breaks copy-paste of email addresses and looks phishy in plain-text clients.
07-H2 (High): SMTP sends credentials in cleartext when useTLS is false
internal/email/smtp_sender.go:218-234
When s.useTLS is false, dispatchSMTP calls smtp.SendMail which performs PlainAuth over an unencrypted connection depending on server behavior. NewSMTPSender only auto-enables TLS for port 587; an operator using port 25, 2525, or any non-587 port with UseTLS:false gets a cleartext credential path.
07-M1 (Medium): SES From display-name missing sanitization note
internal/email/sender.go:295-353
The SES path has no display-name sanitization. SendRegistrationReceivedNotification (templates.go:808) doesn't sanitize the subject before SendToEmailWithCC. Add a doc comment noting SES treats subject/body as opaque structured fields.
07-M2 (Medium): ensureSandboxRecipientVerified leaks un-redacted recipient in error
internal/email/sender.go:286-288
Error strings interpolate raw toEmail verbatim, violating the package's own PII stance. The user-facing message needs the address; the wrapped/logged error should be redacted.
07-M3 (Medium): SendNotification does not sanitize subject before SNS publish
internal/email/sender.go:104-125
Subjects built from user-controlled data (e.g. data.PlanName) can contain newlines, causing an InvalidParameter from SNS at runtime. Apply sanitizeHeader and truncate to 100 chars.
07-L2 (Low): containsColon reinvents strings.Contains
internal/email/factory.go:103-111
Hand-rolled loop; replace with strings.Contains(value, ":") and delete the helper.
07-L3 (Low): warnIfPlaintext heuristic is weak
internal/email/factory.go:89-101
A 25-char password with a colon slips through silently. Strengthen to explicit ARN/GCP resource path/Vault URL prefix check.
07-N2 (Nit): Fixed MIME boundary risks body collision
internal/email/smtp_sender.go:171-172
Generate a per-message random boundary from crypto/rand.
07-N4 (Nit): SendNotification SMTP no-op undocumented
internal/email/smtp_sender.go:75-78
GCP/Azure deployments silently get no broadcast notifications; add a doc note.
Evidence
Report 07 (docs/code-review/07-services-misc.md) findings H1, H2, M1-M3, L2-L3, N2, N4.
Fix
- Add
renderTextTemplateusingtext/templatefor plain-text renderers; keep HTML renderers onhtml/template - In
dispatchSMTP: ifauth != nil && !s.useTLS, return error - Redact email in
ensureSandboxRecipientVerifiederror messages - Apply
sanitizeHeader+ truncate to 100 chars inSendNotification - Add doc comment at
buildSESSendEmailInputabout opaque SES fields - Replace
containsColonwithstrings.Contains - Strengthen
warnIfPlaintextheuristic - Per-message random MIME boundary
- Doc note on SMTP no-op
Files
internal/email/template_renderers.gointernal/email/smtp_sender.gointernal/email/sender.gointernal/email/factory.go
Activity
fix(email): use text/template for plain-text bodies, refuse SMTP auth…
fix(email): SNS subject sanitize+truncate, sandbox PII, warnIfPlainte…
sec(email): stop broadcasting approval tokens via SNS (closes #1015) (#…