Please report security issues privately, not in a public issue:
- Use GitHub's private vulnerability reporting (Security → Report a vulnerability), or
- email me@deepakness.com
Include what you did, what you expected, and what happened. A proof of concept or a copy of the request helps a lot. You will get an acknowledgement, and credit in the fix's commit or advisory if you want it.
Worth knowing before you report something:
- Third-party OAuth credentials (Mastodon, Bluesky, LinkedIn, Threads, X) in D1, encrypted with AES-256-GCM under
APP_ENCRYPTION_KEY. - The account: an email and a PBKDF2-SHA256 hash, written into D1 by
npm run setup. No password is stored anywhere in plaintext. - A TOTP secret and backup codes for that account, encrypted the same way.
- Draft content and uploaded media (R2). Media handed to Meta is served through short-lived signed URLs.
Anything that exposes those — a key leak, a way to read another tenant's row, an SSRF, a signature bypass — is a real finding. Note that this is a single-tenant, self-hosted app: "another user's data" generally means another deployment, not another account on one instance.
- Missing hardening headers on a self-hosted instance where you control the config.
- Anything requiring an already-compromised
APP_ENCRYPTION_KEYorAUTH_SECRET. - The deliberately permissive local-dev escape hatches (
SKIP_TOTP, localhost Mastodon hosts). Both are gated onAPP_URLpointing at localhost.
The main branch is the only supported version; fixes are not backported.
- A flood guard on the endpoints anybody can call.
/api/auth/loginand/api/auth/totp/verifyconsult Cloudflare's Rate Limiting binding: twenty requests a minute per client address, enforced at the edge by the same infrastructure as WAF rate-limiting rules. It is per Cloudflare location and eventually consistent, so it is a burst guard rather than accounting. - A lockout on guessing. Eight wrong passwords in fifteen minutes lock out the client address that sent them (an IPv6 address counts by its /64), and forty from any mix of addresses lock the account, so a guesser can lock out themselves but not the owner. Authenticator codes lock the account after eight failures, counted across attempts rather than per challenge. This, not the edge limit, is what makes guessing impractical.
- Re-authentication for destructive actions. Deleting the account requires the password and a current code, so a stolen session alone cannot wipe an instance.
- Sessions are HMAC-hashed in D1, bound to the stored password hash, idle out after 24 hours (seven days for a browser signed in with "Remember this browser"), and die on a password change.
- Rate-limit the remaining public routes at the edge (Cloudflare → Security → WAF → Rate limiting rules):
/api/internal/tickand/api/healthare reachable without a session. Both do little work per hit; a rule is still the right place to absorb a flood. - Keep
SKIP_TOTPand localhost Mastodon hosts off outside local development — both are gated onAPP_URLpointing at localhost, so a real deployment never enables them by accident.