Security Model
Chat-in-Bio is designed to be safe even when exposed to the public internet. Here's the threat model and defense layers.
Threat model
| Threat | Risk | Mitigation |
|---|---|---|
| Bot abuse | Automated visitors drain LLM tokens | Cloudflare Turnstile on WebSocket connect |
| Message flooding | Single visitor sends excessive messages | Per-visitor message limit (survives reconnects) |
| Token exhaustion | LLM API costs spiral | Daily token budget (DAILY_TOKEN_BUDGET) |
| API abuse | Brute-force admin endpoints | Global rate limit + RBAC with API keys |
| XSS via chat | Malicious input rendered as HTML | A2UI is declarative-only; Lit auto-escapes |
| Credential theft | Integration tokens leaked | Fernet encryption at rest |
| Unauthorized admin access | Attacker modifies content | RBAC with role-based API keys |
| Prompt injection via actions | Attacker injects instructions through UI actions | Context key allowlist + value truncation |
| Insecure defaults | App deployed with placeholder secrets | Startup validation blocks insecure defaults |
| CORS abuse | Cross-origin requests from unauthorized domains | Configurable allowed origins (none by default) |
| Webhook spoofing | Fake webhook payloads trigger actions | HMAC-SHA256 signature verification |
| Invalid API input | Malformed data corrupts DB | Pydantic schema validation on all endpoints |
Defense layers
1. Startup validation
The app refuses to start if SECRET_KEY or ADMIN_API_KEY are left at insecure defaults (changeme, secret, admin, empty, or the placeholder values from .env.example). This prevents accidental production deployments with weak secrets.
2. CORS configuration
CORS is restrictive by default — no cross-origin requests are allowed unless CHATINBIO_ALLOWED_ORIGINS is explicitly configured. The app logs a warning if set to * (allow all).
3. Cloudflare Turnstile
Before a visitor can chat, they must pass a Turnstile challenge. The token is verified server-side on WebSocket connection.
- Configured via
CHATINBIO_TURNSTILE_SECRET_KEY/CHATINBIO_TURNSTILE_SITE_KEY - When not configured, Turnstile is disabled (for local development)
- Verification calls Cloudflare's
siteverifyAPI
4. Rate limiting
Global: Litestar's RateLimitConfig limits all HTTP requests to RATE_LIMIT_PER_MINUTE (default: 60).
Per-visitor: Message counts are tracked per visitor ID (not per WebSocket connection). This means reconnecting doesn't reset the limit. After SESSION_MESSAGE_LIMIT (default: 30) messages within a 1-hour window, the server responds with an error.
5. RBAC (Role-based access control)
All /api/admin/* routes are protected by API key authentication with three roles:
- admin: Full access including key management and deletion
- editor: Create and update access, no deletions
- viewer: Read-only access
The CHATINBIO_ADMIN_API_KEY env var always grants admin role. Additional keys with specific roles can be created via the API or CLI. Keys are stored as SHA-256 hashes — the raw key is shown only once at creation.
Error messages from authorization guards do not disclose the user's current role.
6. Input validation
All admin API endpoints validate request bodies using Pydantic schemas with:
- Required field enforcement
- Type checking and coercion
- String length limits
- Regex pattern validation (e.g.,
product_typemust becourse|book|digital) - Numeric range validation (e.g.,
price_cents >= 0,day_of_week0-6)
Invalid input returns HTTP 400 with field-level error details. Resources that don't exist return HTTP 404 (not 200 with an error body).
7. A2UI safety
A2UI is inherently safe:
- Components are declarative data, not executable code
- The server builds components — the LLM never generates raw HTML
- Lit's rendering engine auto-escapes all text content
- User actions from A2UI components are sanitized before reaching the LLM:
- Only known context keys are forwarded (allowlist:
event_id,product_id, etc.) - Action names are truncated to 50 characters
- Context values are truncated to 200 characters
- Non-dict context is discarded
- Only known context keys are forwarded (allowlist:
8. Credential encryption
Integration credentials (Google OAuth tokens, Gumroad API keys) are encrypted with Fernet before storage:
- The Fernet key is derived from
SECRET_KEYvia SHA-256 - Credentials are encrypted/decrypted on write/read
- The
GET /api/admin/integrationsendpoint returnshas_credentials: true/falseinstead of raw values
9. Webhook authentication
Webhook endpoints support HMAC-SHA256 signature verification:
- Configure
CHATINBIO_GUMROAD_WEBHOOK_SECRETorCHATINBIO_LEANPUB_WEBHOOK_SECRET - When configured, requests must include a valid signature header
- Timing-safe comparison prevents signature timing attacks
10. Safe JSON parsing
All json.loads() calls on database-stored JSON fields are wrapped in try/except with safe fallbacks, preventing crashes from corrupted data.
Production recommendations
For production deployments:
- Set strong secrets — the app enforces this at startup
- Configure CORS — set
CHATINBIO_ALLOWED_ORIGINSto your domain(s) - Always enable Turnstile — it's your primary bot defense
- Configure webhook secrets — if using Gumroad/Leanpub integrations
- Set conservative rate limits — lower
SESSION_MESSAGE_LIMITandDAILY_TOKEN_BUDGETinitially - Put behind a reverse proxy — nginx or Caddy for TLS termination
- Monitor LLM costs — track
token_usagein the Message table - Use RBAC — create
editorkeys for CI/CD andviewerkeys for monitoring