Security Model

How we protect login, sessions, and writes against the usual web hazards.

Discord OAuth2 login

  • Scopes requested: identify + guilds. We never ask for email, connections, or anything that lets us act as the user.
  • The OAuth state is a 256-bit URL-safe random token, single-use, expiring after 10 minutes.
  • State is stored server-side in the signed session cookie, not the URL.
  • The callback drops the code from the URL via a redirect to /servers immediately after exchange.
  • Tokens are never written to disk; we only hold them on the server-side session.

Session cookies

  • HttpOnly: JavaScript can't read it.
  • SameSite=Lax: sites can't auto-submit cross-origin POSTs.
  • Secure when DASHBOARD_BASE_URL is HTTPS, so the cookie never travels over plaintext.
  • The signing key comes from DASHBOARD_SECRET_KEY; if unset we boot with a fresh random one and warn loudly.

CSRF

Every non-safe HTTP method must carry a matching X-CSRF-Token header (or csrf_token form field). Tokens are 256-bit URL-safe randoms, per-session, and compared with hmac.compare_digest. The dashboard's JavaScript wrapper attaches the token automatically.

Authorization layers

  1. Logged in? If not, we redirect to /auth/login.
  2. Token expired? We refresh transparently with the stored refresh token. If refresh fails, the session is wiped.
  3. Bot in guild? The middleware aborts with 404 if the bot isn't actually present.
  4. User has admin? Discord Manage Server or Administrator, intersected with the bot-admin role list.
  5. Write rate limit? Embed sends and ticket-panel posts are rate-limited per actor / guild.

Server-side validation

  • Every role / channel / emoji submitted via the API is verified against the live guild before any write.
  • Embed image URLs must be HTTPS and not point at private IP space (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, link-local).
  • Every JSONB write goes through a per-feature whitelist when one exists.
  • Length caps mirror Discord's: title ≤256, description ≤4096, fields ≤25, etc.

What we don't do

  • We never log access tokens, refresh tokens, or the OAuth state.
  • We never expose another guild's data. Every endpoint scopes by the URL's guild_id and re-checks membership.
  • The dashboard does not accept arbitrary HTML from users. Embed text is rendered as text-with-a-tiny-markdown-subset, never as raw HTML.
Confirm