Conversa holds access to other people's Instagram accounts and to their audiences' messages. This page summarises how it protects them, and how to report a problem.
Please do not open a public issue for a security problem.
Report it privately through GitHub at https://github.com/demogar/conversa/security/advisories/new (on the repository: Security → Advisories → Report a vulnerability). Include what you found, how to reproduce it, and the version or commit. You will get an acknowledgement, and a fix and advisory will be coordinated with you before anything is published.
Only the latest release and main receive security fixes.
| Asset | Threats considered |
|---|---|
| Instagram access tokens, Buffer keys, TOTP seeds | Database leak, backup leak, log leak |
| Team accounts | Credential stuffing, brute force, session theft, CSRF, phishing of one factor |
| Workspace data | A member of one workspace reading or changing another's |
| Webhooks and callbacks | Forged events triggering messages; forged deauthorize or deletion calls |
| Link buttons and postbacks | Forged taps advancing someone else's delivery; click inflation |
| The dashboard | XSS, clickjacking |
Out of scope: an attacker with shell access to the host or to the .env file, and the security of Meta's and Buffer's platforms themselves.
- Passwords hashed with argon2id (19 MiB, 2 iterations, OWASP parameters); 12 to 128 characters. Unknown emails take the same time as wrong passwords.
- Account lockout: every fifth consecutive failure (counted atomically, so parallel guesses all count) locks the account, for 15 minutes first and twice as long each time after, up to 24 hours.
- Two-factor authentication: TOTP (RFC 6238, one step of clock drift), with 8 one-time recovery codes stored as hashes. A TOTP code is accepted once: a replay within its window is rejected, and a recovery code cannot be used twice even by requests racing with it.
- Turnstile (optional) on sign-up, sign-in and password reset, configured by an instance admin in the app.
- Sign-up is invite-only by default after the first account. Only instance admins can open it.
- Instance settings (mail, the Meta app, Turnstile, sign-up) can be read and changed only by instance admins, checked against the database on every request. The first account is the first instance admin, and the last one cannot be removed. Secrets are never returned to the browser, only whether they are set and their last four characters; every change is audited by section, never with values.
- Password reset and invitation tokens are random, single-use, expiring, and stored only as SHA-256 hashes.
- Server-side sessions. The cookie holds a random 256-bit token; the database stores only its SHA-256 hash.
- The cookie is httpOnly, SameSite=Lax, and on HTTPS Secure with the
__Host-prefix (host-only, path/, cannot be set by a subdomain). - 30-day sliding expiry. People can see and revoke their sessions; changing the password signs out the others.
- CSRF: on top of SameSite cookies, every state-changing request must carry an
Originequal toAPP_URL's origin (orSec-Fetch-Site: same-origin). Only signed server-to-server callbacks skip the check. - Rate limits in Valkey, shared by every API process, keyed by the client IP. The web server appends the address it was connected from to
X-Forwarded-For; the API trusts only as many entries asTRUST_PROXYdeclares proxies, and ignores forwarding headers from anything but a private-network peer, so a client cannot pick its own address. Limits: 300 requests per minute in general, 10 per minute on sign-in, sign-up, MFA and reset (blocked 5 minutes after), 5 per 15 minutes for reset emails. - Validation: every body and query goes through a zod schema shared with the web app. JSON bodies are capped at 256 KB.
- Errors are stable codes; stack traces and internal messages never reach clients.
- Every workspace-owned row carries
workspaceIdand every query filters by it. - A global guard checks membership on every
/workspaces/:workspaceId/...route. A workspace you do not belong to answers 404, so ids cannot be probed. - Roles:
MEMBERbuilds automations and uses the inbox;ADMINalso manages channels, integrations, members, invitations and reads the audit log;OWNERalso manages owners and deletes the workspace. A workspace always keeps at least one owner. - An Instagram account can be connected to only one workspace at a time.
- Provider tokens, the Buffer API key, TOTP seeds and the instance settings (including the SMTP password, the Meta app secret, the webhook verify token and the Turnstile secret) are encrypted with AES-256-GCM (random 96-bit IV, authenticated). Each value records which key encrypted it.
- Key rotation: set a new
ENCRYPTION_KEYand keep old keys inENCRYPTION_KEYS_PREVIOUS; old values still decrypt, and the worker re-encrypts channel credentials and instance settings with the current key. See docs/self-hosting.md. - Signing keys are derived per purpose from
SECRET_KEYwith HKDF, so a signature made for one feature is never valid for another.
- Webhooks are verified with the
X-Hub-Signature-256HMAC over the raw body (constant-time comparison). Unsigned or mis-signed requests get401and are not stored. - Deauthorize and data deletion callbacks verify Meta's
signed_request. - OAuth state is signed, expires, and is bound to the person and workspace that started the flow.
- Postback buttons carry a signed delivery reference: nobody can craft a tap that advances another person's delivery.
- Tracked links carry a signed delivery reference. Clicks store a keyed hash of the visitor's IP, never the IP. Redirects send
no-storeandno-referrer.
- A strict Content Security Policy with a per-request nonce and
strict-dynamic;frame-ancestors 'none',form-action 'self',object-src 'none'. The API sendsdefault-src 'none'. X-Frame-Options: DENY,X-Content-Type-Options: nosniff, a restrictivePermissions-Policy,Cross-Origin-Opener-Policy: same-origin, HSTS on HTTPS.- The browser talks to one origin; the API is reached through the web server.
- An audit log records account security events (sign-up, sign-in, password and 2FA changes) and workspace actions (members, invitations, channels, Buffer, automations, inbox replies) with actor, target and, where known, IP. Admins see their workspace's entries in the settings.
- Logs are redacted: cookies,
Authorization, webhook signatures,Set-Cookie, passwords, tokens, API keys and codes are replaced with[redacted], and query strings are never logged.
- Containers run as an unprivileged user; the database and Valkey sit on an internal network with no route outside; only the web port is published, on
127.0.0.1by default. - The sandbox (simulated Instagram) refuses to run in production.
-
APP_URLis HTTPS. -
SECRET_KEYandENCRYPTION_KEYwere generated withpnpm secrets:generateand are not reused elsewhere. -
.envis readable only by the user that runs Docker, and backed up securely. -
TRUST_PROXYmatches your proxy setup (docs/self-hosting.md). - Sign-up is by invitation only (Instance settings → Sign-up) unless you mean to run a public instance.
- Only the people who run the installation are instance admins (Instance settings → Instance admins).
- Owners and admins have 2FA enabled.
- Database backups are encrypted and stored off the host.