Skip to content

Security

timeout edited this page Jul 7, 2026 · 1 revision

Mirrors docs/security.md in the repo.

Security

Token vault

Bot tokens (and Discord webhook URLs) are encrypted with AES-256-GCM (lib/crypto.ts) using a 32-byte key from BOTFLEET_ENCRYPTION_KEY (base64-encoded; generate with openssl rand -base64 32).

  • Encryption/decryption only ever happens inside server-side code (route handlers, the seed script). Nothing client-side ever touches a plaintext or encrypted token.
  • No API response includes tokenEncrypted or a webhook's urlEncrypted - see lib/serialize.ts. There is no "masked" version returned either (e.g. sk-***1234); the field is simply never in the response shape.
  • Token rotation (POST /api/admin/bots/:id/rotate-token) re-encrypts and overwrites in place; the audit log records that a rotation happened, not the token.
  • Nothing in this codebase calls console.log/logs a decrypted secret. Grep for decryptSecret( if you want to verify this yourself - every call site is inside lib/alerts/discord-webhook.ts (to actually POST to Discord) or a route that discards the value after use.

AuthN/AuthZ

  • Admin routes (/admin/* pages, /api/admin/*): guarded by requireAdmin() (lib/require-admin.ts), which checks session.user.role is admin or owner. API routes always return a JSON 401/403 - never a redirect - so a fetch call never silently receives an HTML login page.
  • Customer routes (/api/customer/*): guarded by requireCustomerSession() + loadOwnedBot() (lib/require-customer.ts). loadOwnedBot() returns null - and the route returns a generic 404 - both when a bot doesn't exist and when it exists but belongs to someone else, so bot IDs can't be enumerated to distinguish the two cases.
  • First-run admin bootstrap: BOTFLEET_ADMIN_DISCORD_IDS is a comma-separated allowlist of Discord user IDs. The first time one of those users signs in, auth.ts's session callback promotes them to owner. Additional admins can be promoted afterward at /admin/users - only an owner can change roles there, and the last remaining owner can never be demoted (guarded server-side in PATCH /api/admin/users/:id, not just hidden in the UI).

Content-Security-Policy

Set in next.config.ts for every response. script-src only allows 'unsafe-eval' in development (Next.js Fast Refresh needs it); production builds get script-src 'self' with no eval.

Security Center

GET /api/admin/security (rendered at /admin/security) computes a real report from actual state - see lib/security-checks.ts. Every check either reads an environment variable, queries the database, or reflects a structural guarantee that's true by construction (e.g. "tokens encrypted at rest" is true because there is no plaintext token column in the schema). Nothing is a hardcoded score.

Known accepted risk

npm audit currently reports 2 moderate advisories, both in build/dev tooling, not runtime app code:

  • @hono/node-server (via Prisma's local prisma dev CLI, which this project doesn't use - we run against a real Postgres instance).
  • postcss (bundled inside Next.js's build pipeline).

Both would require downgrading to a much older major version of Prisma or Next.js to "fix," which is a worse trade than the advisory itself. Revisit when upstream ships a patched release.

Reporting a vulnerability

Please don't open a public issue for a security problem. Use GitHub's private vulnerability reporting (Security tab → "Report a vulnerability") on this repository.

Clone this wiki locally