Skip to main content

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

ThreatRiskMitigation
Bot abuseAutomated visitors drain LLM tokensCloudflare Turnstile on WebSocket connect
Message floodingSingle visitor sends excessive messagesPer-visitor message limit (survives reconnects)
Token exhaustionLLM API costs spiralDaily token budget (DAILY_TOKEN_BUDGET)
API abuseBrute-force admin endpointsGlobal rate limit + RBAC with API keys
XSS via chatMalicious input rendered as HTMLA2UI is declarative-only; Lit auto-escapes
Credential theftIntegration tokens leakedFernet encryption at rest
Unauthorized admin accessAttacker modifies contentRBAC with role-based API keys
Prompt injection via actionsAttacker injects instructions through UI actionsContext key allowlist + value truncation
Insecure defaultsApp deployed with placeholder secretsStartup validation blocks insecure defaults
CORS abuseCross-origin requests from unauthorized domainsConfigurable allowed origins (none by default)
Webhook spoofingFake webhook payloads trigger actionsHMAC-SHA256 signature verification
Invalid API inputMalformed data corrupts DBPydantic 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 siteverify API

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_type must be course|book|digital)
  • Numeric range validation (e.g., price_cents >= 0, day_of_week 0-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

8. Credential encryption

Integration credentials (Google OAuth tokens, Gumroad API keys) are encrypted with Fernet before storage:

  • The Fernet key is derived from SECRET_KEY via SHA-256
  • Credentials are encrypted/decrypted on write/read
  • The GET /api/admin/integrations endpoint returns has_credentials: true/false instead of raw values

9. Webhook authentication

Webhook endpoints support HMAC-SHA256 signature verification:

  • Configure CHATINBIO_GUMROAD_WEBHOOK_SECRET or CHATINBIO_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:

  1. Set strong secrets — the app enforces this at startup
  2. Configure CORS — set CHATINBIO_ALLOWED_ORIGINS to your domain(s)
  3. Always enable Turnstile — it's your primary bot defense
  4. Configure webhook secrets — if using Gumroad/Leanpub integrations
  5. Set conservative rate limits — lower SESSION_MESSAGE_LIMIT and DAILY_TOKEN_BUDGET initially
  6. Put behind a reverse proxy — nginx or Caddy for TLS termination
  7. Monitor LLM costs — track token_usage in the Message table
  8. Use RBAC — create editor keys for CI/CD and viewer keys for monitoring