Smartletter
← Accueil

Sécurité

Dernière mise à jour : 23 octobre 2026

Le texte ci-dessous est en anglais, pour les chercheurs en sécurité. Smartletter is a newsletter platform: dashboard, public API, MCP server for AI agents, sending through Amazon SES. This page says how to report a vulnerability, what we protect and how, and what we know is not covered yet.

1. Report a vulnerability

Email security@smartletter.ai. Say what you found, how to reproduce it, the URL or endpoint, and what an attacker gains. A short proof of concept helps.

  • Test only with your own account and workspace. Never read, change or delete another customer's data.
  • Do not send email to addresses you do not own, and do not run denial-of-service, spam or social-engineering tests.
  • Give us a reasonable time to fix the issue before you publish it. We credit reporters who want it.
  • We do not run a paid bug bounty at the moment.

Machine-readable contact: https://smartletter.ai/.well-known/security.txt (RFC 9116).

2. Scope

  • In scope: smartletter.ai (dashboard, public pages, /mcp), api.smartletter.ai (public API), the SMTP relay, the newsletter archives on customer domains, and the code in this repository.
  • Out of scope: the providers themselves (Amazon Web Services, Supabase, Vercel, Cloudflare, Stripe), reports from automated scanners with no demonstrated impact, and the known gaps listed at the end of this page.

3. Tenant isolation

  • Every table of the public schema has Postgres row level security on. Members read the rows of their own workspace through private.is_member_of and private.has_role_in, which only count accepted memberships. Roles: viewer, editor, admin, owner.
  • Sensitive tables (API keys, OAuth tokens and codes, abuse events, sending controls, account deletion codes) have no policy for signed-in users: only the server, with the service role, reads them.
  • Server code that uses the service role filters every query by the workspace of the authenticated caller. The public API and the MCP server take the workspace from the key or token, never from the request.
  • A suspended workspace, or one pending deletion, sends nothing: the sending worker, the public API keys, the MCP server and the internal bridge all require an active workspace.
  • SQL tests in supabase/tests check the policies and grants.

4. Keys, tokens and signatures

  • API keys are sl_ followed by 40 random characters. We store only their SHA-256 hash and an 8-character prefix for display. Three levels: full access, sending only (optionally limited to one domain), signups only. Revoking a key works at once.
  • MCP uses OAuth with PKCE. Access tokens, refresh tokens and client secrets are stored as SHA-256 hashes, refresh tokens rotate, and scopes are mcp:read and mcp:write.
  • Click and open tracking links and the weekly summary opt-out are HMAC-SHA256 signed and checked in constant time.
  • Double opt-in links carry 32 random bytes; only their SHA-256 hash is stored.
  • Outbound webhooks are signed the Standard Webhooks way (svix-id, svix-timestamp, svix-signature, HMAC-SHA256), with two valid secrets during a rotation. Endpoint secrets are encrypted at rest with AES-256-GCM.
  • Amazon SNS notifications (bounces, complaints, inbound mail) are accepted only with a valid signature from an HTTPS certificate on an sns.<region>.amazonaws.com host. Anything else is refused.
  • The public API writes through an internal bridge to the web app, signed with HMAC-SHA256 and a 5-minute window.
  • Deleting an account needs the signed-in owner, a 6-digit code sent by email (hashed, 15 minutes, 5 tries) and the typed workspace name or email. API keys and MCP tokens cannot do it.

5. Rate limits

  • Rate limits run on Upstash Redis. In production, a missing configuration fails closed.
  • Limited: account sign-up and login (per IP and per inbox), public signup forms, lead magnets and archive signups, archive search, AI features, CSV imports, team invitations, and the public API (per workspace and per key).

6. Abuse and deliverability controls

  • Content gate: before a campaign goes out, the worker checks its links with Google Web Risk and looks for phishing wording, brand impersonation, invisible characters and misleading links. A suspect campaign is held for review; without the Web Risk key, campaigns are held rather than sent unchecked.
  • Circuit breaker: a campaign pauses at more than 2% bounces (at least 5) or 0.08% complaints (at least 2). Past 5% bounces (at least 10) or 0.3% complaints (at least 3), the workspace is suspended too. Over 24 hours and 7 days, the same rates warn the owner (first level) or suspend the workspace (second level).
  • Suppression layers: an address that bounced or complained in any workspace is blocked for every workspace; unsubscribes block the workspace; transactional email only skips dead or legally blocked addresses. Repeated soft bounces block for 30 days, then for good on a second strike within 180 days.
  • One account per real inbox (normalized and HMAC-hashed), disposable domains refused, Cloudflare Turnstile on public forms.
  • Imports are checked for list quality, held for review on young workspaces, and can be verified address by address.
  • Sending domains are verified by SES (DKIM and a custom MAIL FROM); a customer cannot mark a domain verified. Workspaces send through their own SES tenant once it is provisioned, so SES can pause one tenant without stopping the others.
  • Inbound mail: the sender is trusted only when DMARC passes.

7. Server-side requests (SSRF)

  • Every URL we fetch for a customer (RSS feeds, webhook endpoints, attachment and image URLs, site analysis) goes through an SSRF guard: a scheme and port policy per use, no credentials in the URL, no internal host names, and no private, loopback or link-local address, literal or resolved.
  • Each redirect hop is checked again, and response size is capped. On Node the socket is pinned to the checked address; on Cloudflare Workers the name is resolved over DNS-over-HTTPS (see the known gaps).

8. Secrets and personal data

  • Secrets live in environment variables and Wrangler secrets, never in the repository.
  • Webhook secrets and online store credentials are encrypted with AES-256-GCM.
  • Error reports sent to Sentry have contact tokens, API keys and bearer tokens removed.
  • Platform admin actions are logged, with an HMAC of the IP address, not the address itself.
  • The owner can delete the account: everything is erased after a 30-day grace period. What we keep is described in the privacy policy: https://smartletter.ai/en/privacy

9. Known gaps

  • Shared IP reputation: unless a workspace takes a dedicated IP, it sends from the shared Amazon SES pool, whose reputation also depends on other SES customers.
  • Gmail complaints are invisible per campaign: Gmail gives no per-message feedback for most senders, and SES replaces our Feedback-ID header, so a Gmail complaint cannot be traced back to one campaign. We only see aggregate rates.
  • The suppression list stores email addresses in plain text, because sending must match them exactly.
  • Unsubscribe and preference tokens are random but stored in plain text in the database.
  • On Cloudflare Workers, the SSRF guard cannot pin the connection: a name with a very short TTL could answer a public address to the check and a private one to the connection (DNS rebinding). Workers have no private network to reach, which limits the impact.
  • There are no read-only API keys: a full-access key can read and write everything in its workspace. MCP tokens can be read-only.
  • Accounts have no multi-factor authentication. Sign-in is by email link or code, so an account is as safe as its mailbox.
  • There is no screen to list or revoke your sessions; signing out ends the current one, and an account deletion signs out the others.
  • Turnstile fails open (no key, timeout or Cloudflare error lets the request through) so that real subscribers are never locked out; the rate limits still apply.
  • The app sets a Content-Security-Policy only on the public archive pages; the dashboard has none.
  • Raw inbound emails kept in S3 are not removed by the account purge.