Skip to content

fix(ses): verify AWS SNS signatures on the SES callback - #466

Open
polylane[bot] wants to merge 1 commit into
mainfrom
polylane/autofix/p5ssbljl4nge
Open

polylane[bot] wants to merge 1 commit into
mainfrom
polylane/autofix/p5ssbljl4nge

Conversation

@polylane

@polylane polylane Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes: useSend SES callback accepts forged SNS events without signature verification

The public SES notification callback trusted any request that named a configured SNS topic ARN, because it never checked the AWS SNS message signature. A forged request could mark email as bounced or complained, unsubscribe contacts that share a recipient address, skew usage counters, and make the server fetch an attacker-controlled URL. The callback now verifies the SNS signature before it processes a message and only follows subscription URLs on the AWS SNS host.

flowchart TD
  A["POST /api/ses_callback"] --> B{"checkEventValidity"}
  B -->|"TopicArn not configured"| R1["401 rejected"]
  B -->|"TopicArn matches"| C{"verifySnsMessageSignature (new)"}
  C -->|"invalid or untrusted cert URL"| R1
  C -->|"valid signature"| D{"Type == SubscriptionConfirmation"}
  D -->|"yes"| E{"isTrustedSnsUrl SubscribeURL (new)"}
  E -->|"untrusted"| R2["400 rejected, no fetch"]
  E -->|"trusted"| F["fetch SNS confirm URL, update setting"]
  D -->|"no"| G["SesHookParser.queue"]
Loading
Detection evidence

Affected: int_13ee3c91c001c2c8xfdj06a9

Why this fix

The route is reachable in production and its only gate was a TopicArn membership check, which is an identifier rather than a secret. AWS requires an HTTPS SNS endpoint to verify the message signature before trusting the payload, and the unverified path reached the suppression and unsubscribe logic in ses-hook-parser.ts. The change adds signature verification and host allowlists at the callback entry point. The one remaining item is optional: getTopicArns still returns every configured ARN, so scoping it per team stays open.

Causal chain
  • Signal (alert): useSend /api/ses_callback processes forged SNS events without signature verification
  • Surfacing site: useSend web app (Railway: useSend) at apps/web/src/app/api/ses_callback/route.ts#POST
  • Mechanism: The POST handler reads req.json() and calls checkEventValidity, which only compares message.TopicArn against the configured topic ARNs; the SNS signature is never verified, so a forged body that names a configured topic ARN passes the gate and reaches SesHookParser.queue.
  • Producer: useSend web app ses_callback route, instance app.usesend.com, at apps/web/src/app/api/ses_callback/route.ts#checkEventValidity
  • Blast radius: unknown other resource(s), unknown other tenant(s); data at risk: Email status, suppression lists, campaign analytics, contact subscriptions, and metering (dailyEmailUsage, cumulatedMetrics) for every tenant sharing the topic ARN set.
  • Producer evidence:
    • checkEventValidity only checks TopicArn membership and returns true; it never reads Signature/SigningCertURL/SignatureVersion (HEAD daaa435)
    • GET https://app.usesend.com/api/ses_callback returns 200 {"data":"Hello"}, endpoint is live
    • handleSubscription fetches message.SubscribeURL with no allowlist or redirect restriction
5 files changed (+559/-12)
  • apps/web/src/app/api/ses_callback/route.api.test.ts: added, +156/-0
  • apps/web/src/app/api/ses_callback/route.ts: modified, +30/-12
  • apps/web/src/server/aws/sns-message-validator.ts: added, +177/-0
  • apps/web/src/server/aws/sns-message-validator.unit.test.ts: added, +194/-0
  • apps/web/src/types/aws-types.ts: modified, +2/-0

Repository lint: pnpm run lint (declared in package.json) fails over the whole repository (the declared target takes no file arguments), so the failure is not attributed to this change and did not block it; run it before merging.

Lint output
: "pnpm.overrides". See https://pnpm.io/settings for the new home of each setting.
@usesend/ui:lint: $ eslint . --max-warnings 0
@usesend/lib:lint: $ eslint . --max-warnings 0
usesend-js:lint: cache miss, executing 2b48c7c6e12d1991
usesend-js:lint: [WARN] The "pnpm" field in package.json is no longer read by pnpm. The following keys were ignored: "pnpm.overrides". See https://pnpm.io/settings for the new home of each setting.
usesend-js:lint: $ eslint . --max-warnings 0
usesend-js:lint: sh: 1: eslint: not found
usesend-js:lint: [ELIFECYCLE] Command failed.
@usesend/ui:lint: [ELIFECYCLE] Command failed.

 Tasks:    1 successful, 3 total
Cached:    0 cached, 3 total
  Time:    3.663s 
Failed:    usesend-js#lint

[ELIFECYCLE] Command failed with exit code 1.
[WARN] The "pnpm" field in package.json is no longer read by pnpm. The following keys were ignored: "pnpm.overrides". See https://pnpm.io/settings for the new home of each setting.
$ turbo lint

Attention:
Turborepo now collects completely anonymous telemetry regarding usage.
This information is used to shape the Turborepo roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://turborepo.dev/docs/telemetry

• turbo 2.10.4
usesend-js#lint:  ERROR  command (/workspace/usesend/useSend/packages/sdk) /root/.local/share/pnpm/.tools/pnpm/11.11.0/bin/pnpm run lint exited (1)
 ERROR  run failed: command  exited (1)

View thread View autofix


Generated by Polylane. You can ask follow-ups by mentioning @polylane in a comment.


Summary by cubic

Verifies AWS SNS message signatures on the SES callback route so forged events are no longer trusted. Previously the route only checked that the TopicArn matched a configured value, so any request naming the right topic could trigger bounce/complaint handling, unsubscribe contacts, skew usage metrics, and fetch an attacker-controlled URL. The callback now verifies the SNS signature and only fetches SubscribeURL values pointing at the AWS SNS host for the message's region. Verification is still skipped in development.

Written for commit 6b8aab8. Summary will update on new commits.

View guided diff Turn on auto-fix

Co-authored-by: polylane[bot] <277585245+polylane[bot]@users.noreply.github.com>
@polylane polylane Bot added polylane severity:high Polylane autofix severity: high labels Oct 15, 2026
@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
unsend-marketing Ready Ready Preview Oct 15, 2026 5:41pm UTC

@coderabbitai

coderabbitai Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 272ed6d4-79ac-49cc-b6fd-87188b4b6b93

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6b8aab8
Status: ✅  Deploy successful!
Preview URL: https://132fe0cc.usesend.pages.dev
Branch Preview URL: https://polylane-autofix-p5ssbljl4ng.usesend.pages.dev

View logs

This branch was successfully deployed

1 active deployment
Preview — 6b8aab8e Deployed Oct 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

polylane severity:high Polylane autofix severity: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant