Skip to content

Threat model

Posthorn’s threat model spans three ingress shapes: HTTP form, HTTP API mode, and SMTP. Each has its own defenses; the upstream transport (Postmark, Resend, Mailgun, SES, outbound-SMTP) is treated as trusted.

This page summarizes what Posthorn defends against, what it doesn’t, and how each defense maps to a configuration setting or code-level guarantee.

#ThreatDefenseConfigurable?
1Drive-by scraper bots filling every form they findHoneypot field — silently 200 on non-empty valueyes — set honeypot
2Direct-POST bots that skip the form pageOrigin/Referer check, fails closed if both headers missingyes — set allowed_origins
3Basic targeted abuse from one or few IPsToken-bucket rate limit per endpoint, per IPyes — set rate_limit
4Provider quota burn / resource exhaustionRate limit + max body sizeyes — set rate_limit, max_body_size
5Email header injection via submitter inputStructured transport-layer guarantee, no string concatenation of headersno — code-level guarantee, test-enforced
6API key theft from logs or error outputAPI keys never loggedno — code-level guarantee, test-enforced
7Cross-Site Request Forgery (CSRF) on form-mode endpointsTokens signed with a Hash-based Message Authentication Code (HMAC) — operator-issued at form-render time, verified on submit. Form-mode only; api-mode endpoints reject csrf_secret at parse time.yes — set csrf_secret
#ThreatDefense
8Unauthorized API mode submissionsAuthorization: Bearer constant-time compare against api_keys list
9Timing attack against the API-key comparecrypto/subtle.ConstantTimeCompare
10Brute-force scan of the API-key spacePer-IP failure budget — 10 failed-auth attempts from one IP within ~1 minute trips the bucket and subsequent failures return 429. Successful auths never consume the budget.
11Public exposure of api-mode endpoint to arbitrary internet scannersDeployment-layer defense — bind to loopback / private network for in-VPS callers, or front public endpoints with Cloudflare Tunnel + Access service tokens. See Deploying API mode safely. Posthorn’s in-process defenses (brute-force lockout, rate limit, constant-time compare) cover what reaches the listener; the listener should not be reachable by arbitrary callers in the first place.
12Duplicate-send from worker retriesIdempotency cache (24h TTL) returns byte-identical replay
13Replay attack via stale idempotency keys24h TTL eviction; in-flight collision returns 409
14Spoofing via leaked API key (per-request from)from is endpoint-configured; not overridable per request
#ThreatDefense
13Open-relay abuse from unauthenticated SMTP clientsAUTH required (PLAIN or client-cert); STARTTLS required by default
14Sender spoofing through SMTPallowed_senders allowlist (exact or *@domain wildcard)
15RCPT bombing (many recipients per session)allowed_recipients allowlist OR max_recipients_per_session cap
16Mass storage exhaustion via large DATAmax_message_size cap (default 1MB)
17Credential leak via plaintext AUTHSTARTTLS required before AUTH; refused otherwise
18Header smuggling via inbound Multipurpose Internet Mail Extensions (MIME) bodyRecipients come from SMTP envelope (RCPT TO), never from MIME To/Cc/Bcc headers

Threats 5, 6, 9, 12, 17, and 18 are code-level guarantees, not config options. There’s no toggle to disable them; the test suite enforces them on every build.

ThreatStatusNotes
Botnet spam from many low-rate IPsv3Rate limit per IP doesn’t catch this. Mitigation: captcha or proof-of-work (v3).
DDoS / Layer 7 attacks(not in scope)This is the CDN/reverse-proxy’s responsibility, not Posthorn’s.
API key theft from misconfigured deploymentdocumentationIf you commit your key to git or expose it in a public env file, Posthorn can’t help. Treat keys as secrets; use secret-injection mechanisms.
Compromise of an upstream transport (Postmark, Resend, etc.)not applicablePosthorn is a thin relay; if the upstream is breached, that’s an upstream incident, not a Posthorn vulnerability.
Supply chain attacks on Posthorn dependenciespartialMinimal dep tree (3 external deps); Dependabot tracks them. No defense beyond keeping deps current.
Phishing emails crafted by submitter contentpartialPosthorn’s body is plaintext; no rendering of submitter HTML. v2 may add markdown rendering with explicit escaping.

What’s a Posthorn vulnerability vs. an operator misconfiguration?

Section titled “What’s a Posthorn vulnerability vs. an operator misconfiguration?”
ClassExamplesWhose responsibility
Posthorn code/spec bugHeader injection passes despite the structural defenses; API key leaks into a log; rate limit bypass via spoofed XFF when trusted_proxies is correctly configuredProject (file an issue)
Operator misconfigurationLoose trusted_proxies (0.0.0.0/0); committed API key to git; no allowed_origins on a public form; running Posthorn directly on port 443 without TLSOperator (see docs)

If you find what you believe is a Posthorn vulnerability, don’t open a public GitHub issue. Email the disclosure address in SECURITY.md.

The layers compose. A bot might pass the honeypot (it didn’t fill the trap) and pass Origin/Referer (it scraped the form first) but trip the rate limit on its 6th submission in a minute. A determined attacker can probably bypass any single layer; the stack is designed so they have to bypass several.

For most public contact forms, this stack catches 95%+ of automated abuse with no operator friction beyond setting four config values. The cases it doesn’t catch are mostly low-rate manual abuse — much rarer in practice than drive-by bots.

See SECURITY.md for:

  • Reporting channel for security issues
  • Supported versions
  • Disclosure timeline expectations
  • Acknowledgment policy