Skip to content

feat(cdp): allowlist SNS TopicArn on the SES webhook#61833

Open
sanidhyasin wants to merge 2 commits into
PostHog:masterfrom
sanidhyasin:harden-ses-webhook-topic-arn-allowlist
Open

feat(cdp): allowlist SNS TopicArn on the SES webhook#61833
sanidhyasin wants to merge 2 commits into
PostHog:masterfrom
sanidhyasin:harden-ses-webhook-topic-arn-allowlist

Conversation

@sanidhyasin

Copy link
Copy Markdown

Problem

The SES event webhook (/public/m/ses_webhook) verifies the SNS message signature, which proves the envelope was signed by some SNS topic — but not that it came from our SES notification topic. The handler also auto-confirms any sns.<region>.amazonaws.com subscription.

That leaves a residual forged-event vector: an attacker can create their own SNS topic, get our endpoint to auto-confirm it, then publish a signed Bounce notification with a forged tracking tag, reaching recipientsManager.optOut for a team. A valid signature alone doesn't stop this.

Closes #61644

Changes

  • New config CDP_SES_WEBHOOK_ALLOWED_TOPIC_ARNS (comma-separated) in nodejs/src/cdp/config.ts, parsed into a Set in SesWebhookHandler.
  • After signature verification and before auto-confirm / notification handling, reject any envelope whose TopicArn isn't in the allowlist (403).
  • Empty default = allow-all, with a one-time warning, so this can ship before the env var is configured per environment — enforcement turns on once the var is set, no code redeploy needed.

Out of scope for this repo (tracked in the issue): wiring the env per environment in charts to the sns_topic_arn output of the ses-events Terraform module, and the separate follow-up to reject unsigned tracking codes.

How did you test this code?

Added unit tests in ses.test.ts covering: a Notification from an allowlisted ARN is processed; one from a non-allowlisted ARN is rejected with 403; whitespace in the comma-separated list is trimmed; a SubscriptionConfirmation from a foreign topic is rejected without auto-confirming (no fetchText call); and an empty allowlist allows any topic.

I did not run the plugin-server test suite locally — its toolchain requires native builds (rdkafka/cyclotron) and backing services — so the new tests have not been executed on my machine and are relying on CI. No manual testing was performed.

A verified SNS signature only proves the envelope was signed by *some*
SNS topic, not that it came from our SES notification topic. An attacker
could get their own topic auto-confirmed and then publish forged (e.g.
Bounce) notifications that reach recipient opt-out.

Reject envelopes whose TopicArn isn't in the new
CDP_SES_WEBHOOK_ALLOWED_TOPIC_ARNS allowlist, after signature
verification and before auto-confirming subscriptions or handling
notifications. An empty allowlist keeps today's allow-all behaviour (with
a one-time warning) so the check can ship before the env var is wired up
per environment.

Closes PostHog#61644
Copilot AI review requested due to automatic review settings June 5, 2026 11:11
@assign-reviewers-posthog assign-reviewers-posthog Bot requested a review from a team June 5, 2026 11:11

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an SNS TopicArn allowlist to the SES webhook handler to prevent accepting (and auto-confirming) SNS messages from untrusted topics.

Changes:

  • Introduces a configurable comma-separated SNS TopicArn allowlist and enforces it in SesWebhookHandler.
  • Logs a warning when the allowlist is empty (permissive mode) and rejects non-allowlisted topics with 403.
  • Adds unit tests covering allowlisted, non-allowlisted, whitespace/comma parsing, and SubscriptionConfirmation behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
nodejs/src/cdp/services/messaging/helpers/ses.ts Adds allowlist parsing, enforcement, and warning behavior in the webhook handler.
nodejs/src/cdp/services/messaging/helpers/ses.test.ts Adds tests validating allowlist enforcement and confirmation-flow safety.
nodejs/src/cdp/config.ts Adds new config key and default value for the allowlist.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nodejs/src/cdp/services/messaging/helpers/ses.ts
Comment thread nodejs/src/cdp/config.ts
Comment thread nodejs/src/cdp/services/messaging/helpers/ses.ts
Comment thread nodejs/src/cdp/services/messaging/helpers/ses.ts
@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
nodejs/src/cdp/services/messaging/helpers/ses.test.ts:310-330
The first two tests exercise the same code path with different inputs and expected outputs — the canonical case for a parameterized test. Per the project's style preference, these should be a single `it.each` block.

```suggestion
        it.each([
            { label: 'allowlisted ARN', topicArn: allowedArn, expectedStatus: 200, expectedMetric: 'email_opened' as const },
            { label: 'foreign ARN', topicArn: foreignArn, expectedStatus: 403, expectedMetric: undefined },
        ])('$label: status=$expectedStatus', async ({ topicArn, expectedStatus, expectedMetric }) => {
            const allowlisted = new SesWebhookHandler(allowedArn)
            const result = await allowlisted.handleWebhook({
                body: notification(topicArn),
                headers: {},
                verifySignature: false,
            })
            expect(result.status).toBe(expectedStatus)
            expect(result.metrics?.[0]?.metricName).toBe(expectedMetric)
        })
```

Reviews (1): Last reviewed commit: "feat(cdp): allowlist SNS TopicArn on the..." | Re-trigger Greptile

Comment thread nodejs/src/cdp/services/messaging/helpers/ses.test.ts Outdated
- Make the empty-allowlist warning module-scoped so it fires at most once
  per process, not once per handler instance.
- Gate the TopicArn check on parsed.mode === 'sns', matching the
  confirmation-flow guard below it.
- Clarify the config comment: the allowlist is independent of SNS
  signature verification.
- Parameterise the allowlisted/non-allowlisted Notification tests with
  it.each.
@sanidhyasin

Copy link
Copy Markdown
Author

Thanks for the reviews — pushed a follow-up addressing the feedback:

  • Warn-once is instance-scoped (Copilot): moved the empty-allowlist warning guard to a module-scoped flag, so it fires at most once per process even if the handler is reconstructed.
  • Misleading config comment (Copilot): reworded — the allowlist is independent of SNS signature verification, which still runs when enabled.
  • Gate on parsed.mode === 'sns' (Copilot): added, matching the confirmation-flow guard just below it.
  • Parameterize the two Notification tests (greptile): consolidated into a single it.each.

On the allow-all-by-default point (hex-security): that's intentional and is the design requested in #61644"Empty default = allow-all (+ warn), so the code can ship before the env is configured; enforcement turns on once charts sets the var per env. No code redeploy needed to enable." The plan is to ship this, then wire CDP_SES_WEBHOOK_ALLOWED_TOPIC_ARNS per environment in charts (to the ses-events Terraform module's sns_topic_arn output), at which point enforcement is on. The now-once-per-process warning makes the permissive state visible until then. Happy to flip the default to deny-all instead if maintainers prefer that posture.

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.

Harden SES webhook: allowlist SNS TopicArn after signature verification

2 participants