Skip to content

Threat model

Frontmail lets you send email straight from the browser. That’s convenient, but it means part of your integration lives in code that anyone can read and copy. This page lists every attack we know of against this model, how Frontmail defends against it, what you should set up yourself, and – openly – the risk that remains. If you find something that’s missing, write to security@frontmail.dev.

  • The public key is public. It’s in your page’s source code. Treat it like a username, not a password. Anyone can copy it and call the API from a script.
  • The Origin header is not authentication. Browsers set it honestly, but a script can send any value. Allowed origins stop other websites from using your key in their visitors’ browsers – they don’t stop a determined attacker.
  • What really protects you is what the attacker can’t change:
    1. the template – the attacker can only send your templates and only fill in their params,
    2. CAPTCHA (Turnstile) – each send costs a human-like solve,
    3. limits – per IP, per key, per organization, per recipient, and your credits.
  • The private key is a secret. It belongs on your server only. It skips the origin allowlist and CAPTCHA and can read your message history.

The rest of this page follows from these four points.

1. Phishing from your domain (the most important one)

Section titled “1. Phishing from your domain (the most important one)”

Attack. A template takes its recipient from params – a password reset, a notification, a newsletter confirmation. The attacker copies your public key and sends that template to a victim, filling reset_url with their own site. The email comes from your domain, passes SPF and DKIM, looks exactly like yours and links to the attacker.

What Frontmail does. A public-key request to a template with a dynamic recipient gets a locked content:

  • CAPTCHA is mandatory (dynamic_recipient_requires_captcha),
  • every link must point to your allowed domains – links in attributes, CSS, plain text (also bare evil.com/login, which mail clients turn into links) and the subject are checked, including tricks like https://you.com@evil.com, backslashes, look-alike Unicode domains and encoded characters (link_not_allowed),
  • From address, From name and Reply-To can’t come from params (dynamic_field_not_allowed),
  • no HTML params, text params at most 500 characters,
  • each recipient gets at most 5 such emails per day from your organization (configurable).

Everything is checked before a credit is used. Auto-replies are locked the same way.

What you should do. Fill in Security → Allowed link domains if your links go anywhere other than your website’s domain. Set up your own Turnstile keys. Templates that only your server sends (order confirmations, password resets generated on your backend) should be sent with the private key.

Remaining risk. The attacker can still send your legitimate email to a stranger – for example “Confirm your newsletter subscription” with a link to your site. It contains nothing they control, so it can’t phish, but it can annoy. The per-recipient limit and CAPTCHA keep this small. Plain text without links (for example “call this number”) in a text param is still possible within 500 characters – keep dynamic text params to what the email really needs.

Attack. A script floods your contact form template with junk.

What Frontmail does. Per-IP limit (default 10 requests / 60 s, IPv6 counted per /64 network), per-key and per-organization limits, anomaly detection, server-side headless-browser and block-list checks, optional CAPTCHA per template.

What you should do. Turn on Require CAPTCHA for public forms. Use the block list for known spam values.

Remaining risk. Without CAPTCHA a patient attacker rotating IP addresses gets through the limits. With CAPTCHA they have to pay a solving service for every email.

3. Calling the API from a script or a fake “app”

Section titled “3. Calling the API from a script or a fake “app””

Attack. The attacker sends requests without a browser, with a forged Origin or with none at all.

What Frontmail does. Requests without Origin and Referer (typical for scripts) are refused unless you turned on Allow mobile apps – also when your allowlist is empty. Only organizations with that option use Frontmail’s shared mobile CAPTCHA; everyone else must pass your own Turnstile widget, and its token must have been issued on the requesting domain.

What you should do. Keep Allow mobile apps off unless you have a native app. Server-side code should use the private key.

Remaining risk. A forged Origin still passes the allowlist; CAPTCHA and the limits are what stop it (see point 2).

Attack. With your public key, the attacker sends as many emails as the limits allow to burn your monthly quota – and, if they could, your overdraft and automatic top-ups.

What Frontmail does.

  • Public-key traffic does not use the overdraft and does not trigger auto top-up unless you explicitly allow it (Billing → Spending by public-key traffic).
  • The hold queue has a cap; beyond it requests are refused instead of queued, so junk can’t eat the credits you buy later.
  • Auto top-up has a monthly count and spending cap.

What you should do. Keep the defaults, use CAPTCHA, watch the anomaly e-mails.

Remaining risk. Your regular quota and credit packs can still be spent by junk that gets past CAPTCHA and limits. That costs you credits, not extra money.

Attack. A cheap flood of invalid requests exhausts your organization’s limits so real visitors get 429.

What Frontmail does. Organization-wide limits and anomaly detection only count requests that passed template and CAPTCHA checks; before that only the per-IP limit and a generous flood guard apply.

Remaining risk. A large distributed flood can still slow things down; the edge firewall (WAF) absorbs most of it.

Attack. Calling Frontmail’s servers directly instead of through the CDN to skip the web application firewall and fake the client IP address.

What Frontmail does. The servers only accept requests that came through our CDN and only trust the client IP address the CDN reports. Nothing for you to configure.

Attack. The private key ends up in browser code (often after migrating from EmailJS, whose accessToken is a private key) and someone copies it.

What Frontmail does. The browser SDKs refuse to run with a private key (private_key_in_browser), and the API refuses private-key requests coming from a web page unless you turned on Allow private keys in browsers. Private keys are stored only as hashes.

What you should do. Never put sk_… into frontend code or into VITE_ / NEXT_PUBLIC_ variables. If it leaked, rotate it immediately.

Remaining risk. Someone holding your private key can send without CAPTCHA and read your message history (including form submissions). Protect it like a password.

Attack. Sending malware to you or to a dynamic recipient as an attachment.

What Frontmail does. Attachments from the browser are off unless the template allows them. Executables, scripts, HTML/SVG, disk images, macro-enabled Office files and similar types are blocked, also when hidden behind tricks like invoice.pdf.exe., right-to-left characters or invisible characters. Public-key uploads have a daily volume limit.

Remaining risk. Ordinary documents (PDF, DOCX) can still carry malicious content; your mail client’s antivirus is the last line.

9. Injecting HTML or script into the email

Section titled “9. Injecting HTML or script into the email”

Attack. Putting <script>, tracking pixels or a fake form into a param.

What Frontmail does. Params are always HTML-escaped (even with {{{ }}}), html params are sanitized by an allowlist and can’t break out of attributes, dangerous URLs are neutralized, line breaks are removed from headers (no header injection), prototype-polluting keys are dropped.

Attack. Using a dynamic-recipient template to send many emails to one address.

What Frontmail does. Per-recipient daily limit (default 5), shared with auto-replies; suppression list for addresses that bounced or complained.

Attack. Password guessing, stolen session, stolen mailbox.

What Frontmail does. Sign-in rate limits, two-factor authentication that is enforced for every sign-in method, password-reset links that work once and sign out all devices, e-mail changes confirmed from the old address, new-device notifications, roles with least privilege and an audit log.

What you should do. Turn on 2FA, give teammates the smallest role that works.

Attack. Reading other people’s message history or files.

What Frontmail does. History is readable only with the private key or by members of your organization; every lookup is scoped to your organization, including files attached to templates.

Attack. Sending fake “bounced” or “complained” events to put your customers on the suppression list.

What Frontmail does. Provider webhooks are verified by signature where the provider supports it and always by a secret URL token; an event only affects a message’s actual recipient.

What you should do. Configure the signing key for your provider where offered.

Attack. Malicious code in the script your website loads.

What Frontmail does. Releases are built without secrets, published with provenance, and CI actions are pinned to exact versions.

What you should do. Load the SDK from a versioned URL with an integrity attribute (see Browser SDK), or bundle the npm package.

Attack. A phishing site uses its own Frontmail account to send stolen passwords to itself – this happens to every service of this kind.

What Frontmail does. Verified owner e-mail, monitoring of unusual traffic, fast suspension of reported accounts. Report abuse to abuse@frontmail.dev.

  • Fill in Allowed origins with your domains.
  • Add your own Turnstile keys and require CAPTCHA on public forms.
  • Use a dynamic recipient only where you need it; fill in Allowed link domains.
  • Keep Allow mobile apps and Allow private keys in browsers off unless you need them.
  • Private key only on the server; rotate it if in doubt.
  • Keep public-key overdraft and auto top-up off (the default).
  • Turn on 2FA for everyone on your team.
  • Load the SDK from a pinned version with integrity.