Security
The real security mechanisms behind Selyf: sessions, CSRF, CORS, token encryption, data isolation, error handling, HTML sanitization, and AI request handling.
This page describes real, implemented controls, not aspirational claims. For the system-level picture these controls fit into, see Architecture. For rate limiting specifically, see Rate Limits.
Sessions & CSRF
Sessions
Sign-in state lives in a single encrypted, HttpOnly cookie (Secure in production, SameSite=Lax) -- there is no separate server-side session store to leak. Signing out or disconnecting ends it immediately.
CSRF protection
State-changing routes (disconnect, account deletion, sync, analyze, draft) require an X-CSRF-Token header matching a session-bound token, checked with a timing-safe comparison, plus an independent check that the request’s Origin header is one of the deployment’s configured web origins. This is a separate mechanism from the OAuth state parameter used during sign-in, and from CORS enforcement below -- three independent layers, not one repeated three times.
CORS
Cross-origin requests are accepted only from an exact, schema-validated origin list -- a wildcard origin is rejected at configuration-load time, before the server even starts, not just at request time.
Gmail access
Read-only scope
Exactly one Gmail OAuth scope is requested: gmail.readonly. Google enforces this boundary at the API level -- there is no application code path that can send, delete, or modify a Gmail message, not merely an absent feature.
Two separate Google credentials
The OAuth client used for sign-in and the GCP service account used to receive Gmail push notifications are distinct credentials with distinct, narrow permissions -- a compromise of one cannot be used to impersonate the other.
Encrypted token storage
The OAuth refresh token is encrypted at rest with AES-256-GCM, using a random initialization vector per encryption operation, before it is ever written to the database.
Pull, not push
Gmail notifications are received by pulling from a Google Cloud Pub/Sub subscription on an interval, not by exposing an inbound webhook URL -- there is no unauthenticated endpoint an attacker could send a forged "new mail" event to.
Data access
IDOR-safe queries
Every query that reads a thread, message, or analysis result is scoped to the authenticated session’s own user id at the database layer -- there is no request shape or parameter that can address another account’s data.
Closed error contract
Every error response is one of a small, fixed set of codes with a generic public message; internal detail (stack traces, query fragments, upstream provider text) never reaches a client. See Errors for the full set.
Content Security Policy
The API's own CSP is deliberately as strict as default-src 'none' and frame-ancestors 'none' -- it returns only JSON, so there is no first-party script, style, or frame for a policy to need to permit. The one exception is the human-facing status page at the API's own root path, which ships zero JavaScript.
Email content & AI
Allowlist-based HTML sanitization
Raw Gmail HTML is sanitized against an explicit tag/attribute allowlist before it is ever stored or rendered -- script, iframe, object, embed, form, and inline event handlers are absent from the allowlist entirely, not merely blocked by a denylist. Every link is forced to open with target="_blank" and rel="noopener noreferrer".
Sandboxed rendering
Sanitized email HTML renders inside a sandboxed iframe using srcDoc, isolated from the dashboard’s own styles and scripts. The sandbox permits allow-same-origin but never allow-scripts, so even a hypothetical sanitizer bypass still could not execute script.
Prompt-injection resistant by construction
Email content is placed inside an escaped, delimited data block in the AI request, separate from a fixed system prompt that never changes based on input -- never treated as an instruction. This is covered by an automated test suite, not just a design intention.
Independently validated AI output
Every AI response is checked against a strict schema on this server, separately from whatever validation the AI provider’s own SDK does -- a malformed or unexpected response is rejected, not passed through. An AI result also has no authority to act: it cannot send, delete, or modify anything.