Skip to content

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

Closed

Description

@cristim

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&#39;Brien, a&b@x.com -> a&amp;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 renderTextTemplate using text/template for plain-text renderers; keep HTML renderers on html/template
  • In dispatchSMTP: if auth != nil && !s.useTLS, return error
  • Redact email in ensureSandboxRecipientVerified error messages
  • Apply sanitizeHeader + truncate to 100 chars in SendNotification
  • Add doc comment at buildSESSendEmailInput about opaque SES fields
  • Replace containsColon with strings.Contains
  • Strengthen warnIfPlaintext heuristic
  • Per-message random MIME boundary
  • Doc note on SMTP no-op

Files

  • internal/email/template_renderers.go
  • internal/email/smtp_sender.go
  • internal/email/sender.go
  • internal/email/factory.go

Activity

  1. added 3 commits that reference this issue on Jun 7, 2026
    439bb3b
    51796db
    acb3a17
  2. added
    pr-createdA PR has been opened for this issue (dedup guard for the auto-PR loop)
    pr-mergedThe PR for this issue has been merged
    on Jun 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/sHoursimpact/all-usersAffects every userpr-createdA PR has been opened for this issue (dedup guard for the auto-PR loop)pr-mergedThe PR for this issue has been mergedpriority/p0Drop everything; same-day fixseverity/highSignificant harmtriagedItem has been triagedtype/securitySecurity findingurgency/nowDrop other things

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions