There are no tagged releases yet. main is the only supported version, and fixes land
there directly.
| Version | Supported |
|---|---|
main |
β |
| Anything older | β β rebase onto main |
Because this is self-hosted, you own the deployed copy. Nothing is pushed to you: if you forked or deployed, watch the repo so you see security fixes when they land.
Please don't open a public issue. Use GitHub's private vulnerability reporting: the Security tab β Report a vulnerability. That opens a private advisory only maintainers can see.
Include what you'd want to receive: the affected file or endpoint, what an attacker gets, and the smallest reproduction you can manage.
Expect an acknowledgement within 5 business days. This is a personal project rather than a staffed product, so please size your expectations for a fix accordingly β but you will get a reply. Coordinated disclosure appreciated; I'll credit you in the advisory unless you'd rather I didn't.
This is a self-hosted mailer. There is no hosted instance to test against, so "in scope" means the code in this repository and the configuration it ships with β not any running deployment.
In scope: authentication and authorization bypass, injection, consent bypass
(anything that sends mail to an address that opted out, or that escalates a narrow
opt-out into a global one), API-key handling, template injection into rendered email,
SSRF, secrets exposure, and any default in wrangler.jsonc that is unsafe in
production.
Out of scope: findings that require DEV_AUTH_BYPASS=true (that is the documented
local-development mode and is explicitly disabled by the production environment);
missing rate limits on Cloudflare-fronted endpoints; and deliverability or spam-filter
behavior, which is a property of your sending domain, not of this code.
Useful context for anyone reviewing:
- The admin console has no password. Cloudflare Access terminates identity at the
edge and forwards a signed JWT.
src/web/auth.tsverifies the signature against the team's live JWKS, pinsalgtoRS256, and checks audience, issuer,expandnbf. Presence of the header proves nothing on its own and is never treated as proof. IfCF_ACCESS_TEAM_DOMAINorCF_ACCESS_AUDis missing, the middleware fails closed and refuses everyone rather than letting anyone through. - API keys are stored as SHA-256 hashes. The plaintext is returned once at
creation and never persisted, so a dumped database does not yield usable keys. Keys
are revoked, never deleted, so the audit trail in
mcp_callskeeps pointing at something real. - Keys are scoped. A
sendkey reaches the transactional API and nothing else. The MCP endpoint requiresadmin. - The MCP endpoint has three gates: an unguessable path secret compared in constant
time (a miss returns
404, not403), anadmin-scoped bearer key, and per-tool guards. Every call β including every refusal β is recorded inmcp_calls. - Irreversible sends require a preflight token.
broadcast_sendandsequence_activaterefuse without a single-use token from their preflight tool, which expires in 10 minutes and is invalidated by any edit to the content or the audience. On top of that,MCP_ALLOW_SENDdefaults to"false"in production: an agent can read and draft everything and still cannot put mail on the wire. - Uploads use a MIME allowlist, not a blocklist β uploaded media is served from the same origin, and an SVG can carry script.
- Everything observable is a row in D1, not a log line. Workers logs expire in 3β7
days;
mcp_callsandsync_runsdo not.
Two steps are load-bearing, and both are easy to get wrong:
- Never
wrangler deploywithout--env production. The top-level vars carryDEV_AUTH_BYPASS=truefor local development. A plain deploy publishes an unauthenticated admin console. - Set
MCP_PATH_SECRETwithwrangler secret put, not as a var. Unset means the MCP endpoint returns404, which is the safe default β turn it on deliberately.
Secrets belong in wrangler secret put or .dev.vars (gitignored). Never in
wrangler.jsonc, and never in .dev.vars.example.