Skip to content

sec(email): sanitize AccountName/Provider in SES registration subject - #566

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
sec/issue-544-ses-subject-injection
May 28, 2026
Merged

cristim merged 3 commits into
feat/multicloud-web-frontendfrom
sec/issue-544-ses-subject-injection

Conversation

@cristim

@cristim cristim commented May 20, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Sanitize data.AccountName and data.Provider with the existing sanitizeHeader helper in the SES registration-subject path (SendRegistrationReceivedNotification in internal/email/templates.go), closing the CRLF email-header-injection vector that PR sec(email): set TLS 1.2 minimum on SMTP StartTLS and sanitize registration subject #523 left open after fixing only the SMTP path.
  • Add defense-in-depth at the data source: validateRegistrationRequest now strips CR/LF and rune-safe length-caps account_name in place, so the cleaned value flows into both persistence and the email.
  • Add regression tests on the SES subject path and the handler normalizer.

Why

Per #401, the SES path is the more dangerous of the two unsanitized sinks: it receives attacker-controlled input from the unauthenticated POST /api/register endpoint. PR #523 wrapped only the SMTP subject in sanitizeHeader, leaving the SES SendEmail subject built from raw AccountName/Provider.

Test plan

  • go build ./...
  • go test ./internal/email/... ./internal/api/... (1471 tests pass)
  • New TestSender_SendRegistrationReceivedNotification_SubjectHeaderInjection mocks SES, captures the SendEmailInput, and asserts the subject reaching SES has no CR/LF and no injected header names.
  • New TestValidateRegistrationRequest_AccountNameSanitized covers CRLF stripping, length capping, and rejection of a CRLF-only name.

Closes #544.

Summary by CodeRabbit

  • Bug Fixes

    • Registration input now strips CR/LF, treats whitespace-only names as empty, and enforces a 256-rune limit for account names.
    • Email notification subjects sanitize account and provider fields to prevent header-injection.
  • Tests

    • Added regression tests for account-name sanitization (CR/LF stripping, rune-safe truncation, empty/whitespace rejection) and for preventing email subject header injection.

Review Change Stack

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-quarter Within the quarter impact/few Limited audience effort/s Hours type/bug Defect type/security Security finding labels May 20, 2026
@coderabbitai

coderabbitai Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 54 minutes and 31 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6764c128-b42b-4e63-8b79-4b48a1b4cb51

📥 Commits

Reviewing files that changed from the base of the PR and between 2556944 and 923720c.

📒 Files selected for processing (4)
  • internal/api/handler_registrations.go
  • internal/api/handler_registrations_validate_test.go
  • internal/email/templates.go
  • internal/email/templates_test.go
📝 Walkthrough

Walkthrough

This PR adds defense-in-depth against CR/LF header injection: it strips CR/LF and caps AccountName length at registration input, and applies header sanitization to AccountName and Provider when composing the SES email subject.

Changes

CRLF Header Injection Mitigation

Layer / File(s) Summary
Registration Input Sanitization
internal/api/handler_registrations.go, internal/api/handler_registrations_validate_test.go
submitRegistration passes a pointer to validateRegistrationRequest, which now strips \r/\n from AccountName, truncates it to maxAccountNameLen (256 runes), and validates required fields. Tests verify CRLF stripping, rune-safe truncation, and rejection of CRLF-only or whitespace-only names.
SES Email Subject Protection
internal/email/templates.go, internal/email/templates_test.go
SendRegistrationReceivedNotification applies sanitizeHeader to data.AccountName and data.Provider when building the SES subject. A regression test asserts the SES subject contains no CR/LF and does not include injected header fragments.

Sequence Diagram

sequenceDiagram
  participant Client
  participant submitRegistration
  participant validateRegistrationRequest
  participant SendRegistrationReceivedNotification
  participant SES
  Client->>submitRegistration: POST /api/register (RegistrationRequest)
  submitRegistration->>validateRegistrationRequest: validate & sanitize *RegistrationRequest (strip CR/LF, truncate)
  validateRegistrationRequest-->>submitRegistration: sanitized request
  submitRegistration->>SendRegistrationReceivedNotification: send notification (sanitized AccountName, Provider)
  SendRegistrationReceivedNotification->>SES: SendEmail with sanitized subject
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #401: Addresses the same header-injection vector by adding input-level CRLF stripping and length-capping plus SES subject sanitization and a regression test.

Suggested labels

priority/p1, severity/high, urgency/this-sprint, impact/many

Poem

A rabbit trims each sneaky line,
Nibbles CR and LF in time,
It checks the name both near and far,
Hops safe subject to the SES star,
🐇✨ Headers clean, no hidden scar.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary security fix: sanitizing AccountName and Provider in the SES registration email subject to prevent header injection.
Linked Issues check ✅ Passed All three acceptance criteria from issue #544 are met: SES subject sanitizes values via sanitizeHeader [files: templates.go], handler CRLF-strips/trims account_name [handler_registrations.go], and regression tests cover both paths.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #544: sanitizing email headers in SES path, defense-in-depth validation in handler, and corresponding regression tests. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sec/issue-544-ses-subject-injection

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/api/handler_registrations_validate_test.go`:
- Around line 3-9: The test currently checks the account name cap using
len(req.AccountName) which measures bytes not runes; update the assertion to use
unicode/utf8.RuneCountInString(req.AccountName) and add an import for
"unicode/utf8" so the cap test verifies rune count (rune-safe) for
req.AccountName instead of byte length; keep the same assertion semantics but
replace len(...) with utf8.RuneCountInString(...).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81f67947-ffd7-4aee-9b01-ab738f2a4f3f

📥 Commits

Reviewing files that changed from the base of the PR and between bc7bf0f and 2a1192f.

📒 Files selected for processing (4)
  • internal/api/handler_registrations.go
  • internal/api/handler_registrations_validate_test.go
  • internal/email/templates.go
  • internal/email/templates_test.go

Comment thread internal/api/handler_registrations_validate_test.go
@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

Addressed in 5ecc6cf: the length-cap test now uses a multibyte rune and asserts via utf8.RuneCountInString (plus utf8.ValidString), so it genuinely exercises the rune-safe cap rather than relying on ASCII byte==rune equivalence. Good catch.

@cristim
cristim force-pushed the sec/issue-544-ses-subject-injection branch 2 times, most recently from 5ecc6cf to 9bf5707 Compare May 20, 2026 16:39
@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai resolve

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

@cristim

cristim commented May 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim force-pushed the sec/issue-544-ses-subject-injection branch from 9bf5707 to 32f6ecd Compare May 22, 2026 13:06
@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

CR finding #discussion_r3275573192 - confirmed resolved.

The fix was in commit 9bf5707 (now rebased to 32f6ecd on current base):

  • Test in internal/api/handler_registrations_validate_test.go now imports unicode/utf8 and asserts utf8.RuneCountInString(req.AccountName) <= maxAccountNameLen with a multibyte input (strings.Repeat("e-acute", maxAccountNameLen+50)), so the rune-safety cap is actually exercised.
  • Production code (validateRegistrationRequest) already capped by runes via []rune(req.AccountName) - the bug was only in the test assertion.
  • Branch rebased onto origin/feat/multicloud-web-frontend (force-with-lease). All tests pass: go test ./internal/api/... (4 passed), go test ./internal/email/... (316 passed), go vet clean, gofmt clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the detailed confirmation. The rune-safety fix is clean — using utf8.RuneCountInString with a genuine multibyte input (é repeated past the cap) correctly exercises the []rune(req.AccountName) slicing in production, making the test truly validate rune-aware truncation rather than just byte counts. Triggering the new review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/api/handler_registrations.go (1)

408-419: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject whitespace-only account_name after CR/LF normalization.

After CR/LF stripping, inputs like " \r\n " become whitespace-only and currently pass Line 418’s emptiness check. Trim before required validation so effectively empty names are rejected.

Proposed fix
 func validateRegistrationRequest(req *RegistrationRequest) error {
 	req.AccountName = strings.NewReplacer("\r", "", "\n", "").Replace(req.AccountName)
+	req.AccountName = strings.TrimSpace(req.AccountName)
 	if r := []rune(req.AccountName); len(r) > maxAccountNameLen {
 		req.AccountName = string(r[:maxAccountNameLen])
 	}
@@
 	if req.AccountName == "" {
 		return NewClientError(400, "account_name is required")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/handler_registrations.go` around lines 408 - 419, Normalize
req.AccountName with CR/LF stripping as done, then immediately call
strings.TrimSpace on the result and assign back to req.AccountName before doing
the rune-length truncation and the emptiness check; ensure the truncation (the
rune slice using maxAccountNameLen) and the subsequent validation that returns
NewClientError(400, "account_name is required") operate on the trimmed value so
whitespace-only names are rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/api/handler_registrations.go`:
- Around line 408-419: Normalize req.AccountName with CR/LF stripping as done,
then immediately call strings.TrimSpace on the result and assign back to
req.AccountName before doing the rune-length truncation and the emptiness check;
ensure the truncation (the rune slice using maxAccountNameLen) and the
subsequent validation that returns NewClientError(400, "account_name is
required") operate on the trimmed value so whitespace-only names are rejected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61dcac91-69f8-4c2d-9759-5ca6a20acb3e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf5707 and 32f6ecd.

📒 Files selected for processing (4)
  • internal/api/handler_registrations.go
  • internal/api/handler_registrations_validate_test.go
  • internal/email/templates.go
  • internal/email/templates_test.go

cristim added a commit that referenced this pull request May 25, 2026
After stripping CR/LF the account_name could still be whitespace-only
(spaces, tabs), which passed the empty-string guard and produced an SES
subject line containing `Account "": `. Add strings.TrimSpace after the
CR/LF replacement so the existing empty-string check also catches that
case.

Adds a regression test: POST with account_name "   \t  " must return
400 with "account_name is required".

Closes finding on PR #566 (CR review 2026-05-22).
@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cristim added 3 commits May 28, 2026 16:01
PR #523 fixed only the SMTP registration-subject path. The SES path
(SendRegistrationReceivedNotification in internal/email/templates.go) still
built the "CUDly - New Account Registration: %s (%s)" subject from raw,
attacker-controlled data.AccountName and data.Provider sourced from the
unauthenticated POST /api/register endpoint, then passed it to the SES
SendEmail API. A CR+LF in account_name could inject extra email headers.

Wrap both fields in the existing sanitizeHeader helper (reused from the SMTP
path) and add defense-in-depth at the data source: validateRegistrationRequest
now strips CR/LF and length-caps account_name in place. Adds regression tests
on the SES subject path and the handler normalizer.

Closes #544.
CodeRabbit flagged that the length-cap test measured byte length while the
implementation caps by rune count, so the ASCII-only input never exercised
rune-safety. Switch the input to a multibyte rune, assert via
utf8.RuneCountInString, and verify the truncated result is still valid UTF-8.
After stripping CR/LF the account_name could still be whitespace-only
(spaces, tabs), which passed the empty-string guard and produced an SES
subject line containing `Account "": `. Add strings.TrimSpace after the
CR/LF replacement so the existing empty-string check also catches that
case.

Adds a regression test: POST with account_name "   \t  " must return
400 with "account_name is required".

Closes finding on PR #566 (CR review 2026-05-22).
@cristim
cristim force-pushed the sec/issue-544-ses-subject-injection branch from 2556944 to 923720c Compare May 28, 2026 14:01
@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit 4956d66 into feat/multicloud-web-frontend May 28, 2026
4 checks passed
@cristim
cristim deleted the sec/issue-544-ses-subject-injection branch June 3, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect type/security Security finding urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant