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