Skip to content

fix(security): remove unauthenticated open email relay (SES) - #2862

Merged
hanapotski merged 1 commit into
developfrom
fix/email-open-relay
Aug 22, 2026
Merged

fix(security): remove unauthenticated open email relay (SES)#2862
hanapotski merged 1 commit into
developfrom
fix/email-open-relay

Conversation

@hanapotski

Copy link
Copy Markdown
Contributor

Summary

Fixes security audit finding #3 (Critical)POST /api/emails/ was an open email relay. It read emailFrom, emailTo, subject, htmlBody, and textBody straight from the request body with no auth, no Source allowlist, and no rate limiting, then passed emailFrom through as the SES Source. Anyone could send arbitrary phishing/spam email, to any recipient, "From" any address, on the project's verified SES identity — risking AWS billing abuse and domain deliverability/reputation being blacklisted.

Why remove rather than lock down

The generic relay has no legitimate caller — not the client, the embed, or the server. The only references were in the thunder-tests/ manual API collection (a dev tool). The public contact form (POST /api/emails/contact) already covers the real use case safely: a fixed template with a server-controlled Source (EMAIL_USER) and a server-side staff recipient. Bolting auth onto a capability nothing uses would just retain a dangerous arbitrary-send primitive, so the relay is removed entirely.

Changes

  • Drop the POST / route (email-router.ts).
  • Remove the send controller handler (email-controller.ts).
  • Remove ses-service.send — the arbitrary-Source sender.
  • Remove the now-orphaned Email type (email-type.ts).

Unaffected

POST /api/emails/contact and all templated system emails (registration confirmation, password reset) are unchanged — they already use a server-controlled Source.

Verification

tsc --noEmit and npm run lint both pass clean. No email unit tests exist to update.


🤖 This PR was written by Claude on behalf of @hanapotski.

🤖 Generated with Claude Code

POST /api/emails/ took emailFrom, emailTo, subject, htmlBody and
textBody straight from the request body -- with no auth, no allowlist on
the SES Source, and no rate limiting -- and passed emailFrom through as
the SES Source. This turned the app's verified SES identity into an open
relay: anyone could send arbitrary phishing/spam email, to any
recipient, "From" any address, at the project's expense and reputation
(security audit finding #3, Critical).

The generic relay has no legitimate caller (client, embed, or server) --
the only references were in the thunder-tests manual collection. The
public contact form (POST /api/emails/contact) already covers the real
use case safely: a fixed template with a server-controlled Source
(EMAIL_USER) and server-side staff recipient.

Rather than bolt auth onto a capability nothing uses, remove it entirely:
- Drop the POST / route.
- Remove the send controller handler.
- Remove ses-service.send (the arbitrary-Source sender).
- Remove the now-orphaned Email type.

/contact and all templated system emails (registration, password reset)
are unchanged. tsc and lint pass clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@VirginiaWu11 VirginiaWu11 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.

Clean fix — reviewed the full diff plus the surrounding call graph, no issues found.

Verified:

  • No remaining callers of the removed send (email-controller.ts), emailService.send (ses-service.ts), or the Email type anywhere in server/, client/, or embed/ — grepped for all three. account-service.ts only imports sendRegistrationConfirmation/sendResetPasswordConfirmation from ses-service.ts, never send.
  • SendEmailCommandOutput stays correctly imported in email-controller.ts since sendContactForm still uses it in its type signature — nothing orphaned.
  • Every remaining SES sender (sendContactEmail, registration/reset-password confirmations, etc.) sources Source from the server-controlled EMAIL_USER env var, not from request input — confirms the removed send really was the only arbitrary-Source primitive.
  • POST /api/emails/contact is untouched and still mounted correctly under /api/emails in routes/index.ts.
  • tsc --noEmit passes clean on the resulting tree.

Removing the relay outright (rather than bolting on auth) is the right call here since nothing legitimate called it. Approving.


🤖 Review generated with Claude Code

@hanapotski
hanapotski merged commit ece7be2 into develop Aug 22, 2026
1 check passed
@hanapotski
hanapotski deleted the fix/email-open-relay branch August 22, 2026 05:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants