Skip to content

Security Overview

SQLatte assumes every SQL statement it executes was generated by an LLM, not typed by a trusted operator — and validates accordingly, on every entry point (chat UI, embedded widgets, and MCP alike).

SQL Injection Protection

Two independent layers in src/core/sql_validator.py, run before the database is ever touched:

Function Purpose
is_select_only(sql) Hard enforcement gate. Rejects anything that isn't a SELECT/WITH, any write keyword (INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER, CREATE, GRANT, REVOKE, EXEC, CALL, MERGE, LOAD, COPY, …), and dangerous functions (pg_read_file, dblink, xp_cmdshell, lo_export, pg_sleep, http_get, EXTERNAL_QUERY, …)
risk_score(sql) Observability only — never blocks. A 0–100 additive score logged to every audit record, regardless of whether the query was allowed

String literals and comments are stripped before both checks, closing the obvious obfuscation bypass (hiding a keyword inside a quoted string or comment).

This applies identically to the main chat endpoint and the MCP/auth-widget endpoint — the bypass_intent flag MCP uses only skips intent classification (is this a data question or small talk), never SQL validation.

Risk score is additive: dangerous keywords add up to +50, dangerous functions up to +80 (weighted higher — they can exfiltrate data even inside a valid SELECT), and for queries that pass validation, exposure signals like SELECT * (+5), missing LIMIT (+5), UNION/INTERSECT (+10), deep subqueries (+5/level, max +15), and CROSS JOIN (+10) apply. A clean, limited SELECT scores 0; the ceiling for anything that actually executes is 45. See the Audit Logs page for how the score surfaces in practice.

Credential Isolation

Database credentials live server-side, in config.yaml or config_db, never in a client. MCP clients and embedded widgets authenticate with short-lived session tokens instead — see API Tokens & Admin Auth. Revoking access means invalidating a token, not rotating a shared password.

Field Masking (MCP)

Sensitive columns (emails, phone numbers, etc.) can be hashed, partially redacted, or fully redacted before results reach an MCP client — alias-aware, so an LLM can't rename a column to dodge the rule. See Field Masking.

Rate Limiting

Configurable per-endpoint request throttling (src/core/rate_limiter.py), three strategies:

rate_limiting:
  enabled: true
  strategy: sliding_window   # sliding_window | token_bucket | fixed_window
  key_type: session_id       # session_id | ip | user_id
  requests_per_window: 30
  window_seconds: 60
  protected_paths: ["/query", "/auth/query"]
  exclude_paths: ["/health", "/static", "/admin"]

Limits are enforced in-memory per process and keyed by session, IP, or user depending on key_type.

Multi-Tenant Auth Plugin

The plugins.auth block enables per-user database connections with catalog/schema restrictions, so different users can be scoped to different parts of the warehouse from a single deployment:

plugins:
  auth:
    enabled: true
    session_ttl_minutes: 480
    allowed_catalogs:
      - name: "hadoop"
        allowed_schemas: ["hive"]
      - name: "network"
        allowed_schemas: ["cloudflare"]
    allowed_db_types: ["trino"]

Admin login itself is separately brute-force protected (10 attempts / 5-minute window → 10-minute lockout per IP) and fails closed: if admin.password is unset, the admin panel refuses to authenticate anyone rather than falling back to an open door.

Trino Catalog Lock and Discovery Tokens

Two related, Trino-only controls under plugins.auth:

  • enforce_catalog_lock (on by default): rejects generated SQL that references a Trino catalog other than the one the session/token is scoped to — closes off an MCP client steering ask_database at a foreign catalog. No effect without allowed_catalogs configured, or on non-Trino providers.
  • enable_discovery_tokens (off by default): drops the catalog/schema picker from the token screen entirely — sign-in becomes username/password only, and every token from it is scoped by allowed_catalogs instead of one fixed catalog. This unlocks:
    • The MCP discover_tables tool (network/SSE MCP mode only — see MCP Overview): cross-catalog table/column search by partial name, metadata only, no row data.
    • POST /auth/discovery-token — issue a token directly from Trino credentials
    • POST /auth/discover — search by session
    • POST /auth/token/generate-discovery — issue a discovery token from an active session
    • POST /admin/discovery-token — admin-issued, backend-only (no self-service UI)
    • A catalog-less token's list_tables, get_schema, and ask_database are all automatically scoped to allowed_catalogs, returning/accepting fully-qualified catalog.schema.table names instead of erroring for having no default catalog.

This is fully backward compatible: a token minted before discovery was enabled still carries its fixed catalog and behaves exactly as before.

LDAP / Active Directory Login

Disabled by default (ldap.enabled: false). When enabled, LDAP is used for:

  • Admin login — tried first; falls back to admin.username/password if the directory is unreachable or the bind fails, so a directory outage never locks admins out entirely.
  • The Assistant login gate (plugins.assistant_login.enabled: true) — an optional login screen in front of the legacy /query Assistant widget, which otherwise has no login at all. A successful LDAP bind is an identity check only: the issued session still uses the server's own database config, not per-user credentials (unlike the plugins.auth multi-tenant plugin above).

Supports direct bind (user_dn_template) or search+bind with a service account (bind_dn/bind_password/base_dn/user_search_filter), plus an optional required_group_dn membership check. Both ldap and plugins.assistant_login are editable from the Admin Panel's LDAP / SSO tab, with optional persistence to config_db. See Full Config Reference.

Login Types Comparison

Access path Credential held by client Scope
Chat UI (default widget) None — uses server config Whatever the server's configured DB connection allows
Chat UI (with LDAP gate) LDAP username/password, then a session Same server DB connection as above — LDAP is identity, not per-user DB access
Auth widget Session token (via /auth/login) Per-user, restricted to allowed_catalogs/allowed_schemas
Auth widget (discovery token) Session token, no fixed catalog Every catalog/schema in allowed_catalogs
MCP (token mode) API token Same as the token owner's auth-widget session
MCP (legacy password mode) Raw DB credentials in local config Same as the DB user's own permissions
Admin panel Admin session cookie (LDAP or local) Full configuration access