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.
In scope: HTTP form ingress
Section titled “In scope: HTTP form ingress”| # | Threat | Defense | Configurable? |
|---|---|---|---|
| 1 | Drive-by scraper bots filling every form they find | Honeypot field — silently 200 on non-empty value | yes — set honeypot |
| 2 | Direct-POST bots that skip the form page | Origin/Referer check, fails closed if both headers missing | yes — set allowed_origins |
| 3 | Basic targeted abuse from one or few IPs | Token-bucket rate limit per endpoint, per IP | yes — set rate_limit |
| 4 | Provider quota burn / resource exhaustion | Rate limit + max body size | yes — set rate_limit, max_body_size |
| 5 | Email header injection via submitter input | Structured transport-layer guarantee, no string concatenation of headers | no — code-level guarantee, test-enforced |
| 6 | API key theft from logs or error output | API keys never logged | no — code-level guarantee, test-enforced |
| 7 | Cross-Site Request Forgery (CSRF) on form-mode endpoints | Tokens 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 |
In scope: HTTP API mode
Section titled “In scope: HTTP API mode”| # | Threat | Defense |
|---|---|---|
| 8 | Unauthorized API mode submissions | Authorization: Bearer constant-time compare against api_keys list |
| 9 | Timing attack against the API-key compare | crypto/subtle.ConstantTimeCompare |
| 10 | Brute-force scan of the API-key space | Per-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. |
| 11 | Public exposure of api-mode endpoint to arbitrary internet scanners | Deployment-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. |
| 12 | Duplicate-send from worker retries | Idempotency cache (24h TTL) returns byte-identical replay |
| 13 | Replay attack via stale idempotency keys | 24h TTL eviction; in-flight collision returns 409 |
| 14 | Spoofing via leaked API key (per-request from) | from is endpoint-configured; not overridable per request |
In scope: SMTP ingress
Section titled “In scope: SMTP ingress”| # | Threat | Defense |
|---|---|---|
| 13 | Open-relay abuse from unauthenticated SMTP clients | AUTH required (PLAIN or client-cert); STARTTLS required by default |
| 14 | Sender spoofing through SMTP | allowed_senders allowlist (exact or *@domain wildcard) |
| 15 | RCPT bombing (many recipients per session) | allowed_recipients allowlist OR max_recipients_per_session cap |
| 16 | Mass storage exhaustion via large DATA | max_message_size cap (default 1MB) |
| 17 | Credential leak via plaintext AUTH | STARTTLS required before AUTH; refused otherwise |
| 18 | Header smuggling via inbound Multipurpose Internet Mail Extensions (MIME) body | Recipients 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.
Out of scope
Section titled “Out of scope”| Threat | Status | Notes |
|---|---|---|
| Botnet spam from many low-rate IPs | v3 | Rate 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 deployment | documentation | If 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 applicable | Posthorn is a thin relay; if the upstream is breached, that’s an upstream incident, not a Posthorn vulnerability. |
| Supply chain attacks on Posthorn dependencies | partial | Minimal dep tree (3 external deps); Dependabot tracks them. No defense beyond keeping deps current. |
| Phishing emails crafted by submitter content | partial | Posthorn’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?”| Class | Examples | Whose responsibility |
|---|---|---|
| Posthorn code/spec bug | Header injection passes despite the structural defenses; API key leaks into a log; rate limit bypass via spoofed XFF when trusted_proxies is correctly configured | Project (file an issue) |
| Operator misconfiguration | Loose 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 TLS | Operator (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.
Defense in depth
Section titled “Defense in depth”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.
Disclosure
Section titled “Disclosure”See SECURITY.md for:
- Reporting channel for security issues
- Supported versions
- Disclosure timeline expectations
- Acknowledgment policy