Skip to content

Security: Chinchill-AI/chat-sdk-python

docs/SECURITY.md

Security

Security decisions, implementation details, and known limitations for chat-sdk-python.

Webhook Verification Per Platform

Every adapter verifies incoming webhook requests before processing. The verification method depends on the platform.

Slack

  • Method: HMAC-SHA256 signature verification
  • Header: X-Slack-Signature (v0 format: v0=<hex-digest>)
  • Timestamp: X-Slack-Request-Timestamp (rejected if > 5 minutes old to prevent replay attacks)
  • Signing key: signing_secret from the Slack app configuration
  • Base string: v0:{timestamp}:{body}
  • Comparison: Timing-safe via hmac.compare_digest()
# Simplified verification flow in Slack adapter
base_string = f"v0:{timestamp}:{body}"
expected = "v0=" + hmac.new(
    signing_secret.encode(), base_string.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature_header):
    raise AuthenticationError("slack", "Invalid signature")

Discord

  • Method: Ed25519 signature verification (using PyNaCl)
  • Headers: X-Signature-Ed25519, X-Signature-Timestamp
  • Public key: public_key from the Discord application
  • Verified message: {timestamp}{body}
  • Library: nacl.signing.VerifyKey

Teams

  • Method: JWT token validation (RS256)
  • Header: Authorization: Bearer <jwt>
  • Issuer: https://api.botframework.com or https://sts.windows.net/{tenant-id}/
  • Audience: The app's app_id
  • JWKS endpoint: https://login.botframework.com/v1/.well-known/openidconfiguration
  • Key rotation: JWKS keys are cached with a configurable TTL. See "Known Limitations" below.

The Teams adapter implements its own JWT validation rather than using the Microsoft Bot Framework SDK because there is no maintained async Python equivalent. The validation includes issuer checking, audience verification, expiry checking, and signature verification against the JWKS-provided RSA public keys.

Telegram

  • Method: Secret token comparison
  • Header: X-Telegram-Bot-Api-Secret-Token
  • Comparison: Timing-safe via hmac.compare_digest()
  • Required in webhook mode (since the 4.41 wave; upstream chat@4.39, vercel/chat#858): a webhook-mode adapter without secret_token / TELEGRAM_WEBHOOK_SECRET_TOKEN fails to construct (mode="webhook") or to initialize (mode="auto" resolving to webhook), and handle_webhook returns 401 before reading the body. The only way to accept unverified webhooks is the explicit opt-out allow_unverified_webhooks=True or TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS=true (exact string). Polling mode needs neither.
  • Redelivery dedupe: each accepted update's integer update_id is claimed in the state adapter (telegram:webhook-update:{sha256(bot_user_id)}:{update_id}, 24h TTL) before dispatch, so a Telegram retry (including a callback_query button action) is dispatched once. A state or bot-identity failure returns 503 without dispatch so Telegram retries later.

WhatsApp (Meta Cloud API)

  • Method: HMAC-SHA256 signature verification
  • Header: X-Hub-Signature-256 (format: sha256=<hex-digest>)
  • Signing key: app_secret from the Meta app
  • Comparison: Timing-safe via hmac.compare_digest()

Google Chat

  • Method: Google-signed JWT verification, bound to a configured identity for each transport (#222)
  • Header: Authorization: Bearer <jwt>
  • Direct webhooks, "Project number" audience (google_chat_project_number): a JWT self-signed by chat@system.gserviceaccount.com. It is verified against that account's X.509 certs (https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com), with issuer chat@system.gserviceaccount.com and audience equal to the project number.
  • Direct webhooks, "HTTP endpoint URL" audience (endpoint_url): a Google OIDC ID token verified against https://www.googleapis.com/oauth2/v3/certs. The issuer must be accounts.google.com or https://accounts.google.com, and the audience must equal the configured endpoint_url exactly. email_verified must be true, and email must be chat@system.gserviceaccount.com or the exact workspace_add_on_service_account_email. When both direct verifiers are configured, the endpoint-URL check runs first, then the project-number check.
  • Pub/Sub pushes (pubsub_audience): the same OIDC checks with audience equal to pubsub_audience. email_verified must be true, and email must equal pubsub_service_account_email. If that setting is unset, every push is rejected.
  • Why identity matters: none of the audiences is secret. Anyone can get Google to sign a token for a public URL, so only the email claim (or the Chat issuer's own key) identifies the sender.
  • Key caching: both key sets are fetched asynchronously and cached for 1 hour. A failed fetch is not cached.
  • Token lifetime: exp and iat are required and checked with 300 s of clock skew. A token whose exp is 24 hours or more in the future is rejected, as google-auth-library does.
  • Transports verify independently: a request shape whose verifier is not configured is rejected with 401 unless disable_signature_verification is set. The constructor refuses to start when no verifier is configured and the opt-out is not set.
  • A configured verifier beats the opt-out: disable_signature_verification only covers a transport that has no verifier. Setting endpoint_url (even only for button routing) makes it a direct-webhook verifier, so direct webhooks are verified and the opt-out no longer applies to them. The constructor logs a warning when both are set.
  • Endpoint inference: when endpoint_url is unset, the button-click routing URL is inferred from request.url, but only after a direct webhook passes verification. It is never used as a verification audience.
  • Bot identity (#223): bot_user_id / GOOGLE_CHAT_BOT_USER_ID (the app's canonical users/... name) is the only source of "self". It is never learned from inbound mentions or read back from state, so another bot cannot become "self". When it is unset, every BOT sender is treated as self (no reply loops, but other bots' messages are ignored) and no bot mention is normalized.
  • Attachment downloads (#223): bytes come only from the Chat media API (/v1/media/{resourceName}?alt=media). The event's downloadUri is display-only and never fetched, so the service-account token is never sent to a URL taken from a payload or from persisted fetch_metadata. A resourceName with ?, #, %, backslash, anything outside printable ASCII, or a . / .. segment is rejected before a token is minted.
  • Message ids bound to the thread's space (#223): edit_message, delete_message, add_reaction, remove_reaction and fetch_message require a full spaces/{space}/messages/{message} name in the thread's own space and raise ValidationError otherwise, before any API call.

GitHub

  • Method: HMAC-SHA256 signature verification
  • Header: X-Hub-Signature-256 (format: sha256=<hex-digest>)
  • Signing key: webhook_secret from the GitHub app
  • Comparison: Timing-safe via hmac.compare_digest()

Linear

  • Method: HMAC-SHA256 signature verification
  • Header: Linear-Signature
  • Signing key: webhook_secret from the Linear app
  • Comparison: Timing-safe via hmac.compare_digest()

SSRF Protections

Teams service_url Validation

The Teams adapter receives a serviceUrl in every activity payload. This URL is used to send replies back to Teams. The adapter validates that the URL matches the expected Microsoft domains:

  • https://*.botframework.com/
  • https://smba.trafficmanager.net/

Requests to arbitrary URLs (which could target internal services) are rejected.

WhatsApp Media URL Validation

WhatsApp media downloads go through the shared guarded downloader (chat_sdk.shared.download: https only, internal addresses refused after DNS resolution, redirects re-validated, 25 MB / 30 s limits). The access token is attached per request hop, only when that hop's URL is:

  • https://fbcdn.net/ or https://fbsbx.com/ or a subdomain of either, on the default port, or
  • the exact configured Graph API origin (scheme, host and port).

Any other media URL is refused before the token is sent. Messenger attachment downloads use the same downloader, restricted to fbsbx.com / fbcdn.net (and subdomains), with no credentials.

Slack response_url Validation

Slack's response_url (used for responding to slash commands and interactive messages) is validated to ensure it points to Slack's response-URL hosts (port of upstream isTrustedSlackResponseUrl, vercel/chat#876). The check runs when an ephemeral message id is encoded, when it is decoded, and again right before the request is sent, and the SDK-free send_slack_response_url primitive applies the same check:

  • scheme https, no userinfo, no explicit port
  • host exactly hooks.slack.com or hooks.slack-gov.com (exact match, never a suffix match)

Crypto: AES-256-GCM for Slack Token Encryption

The Slack adapter supports multi-workspace OAuth installations. Bot tokens for each workspace can be encrypted at rest using AES-256-GCM.

Implementation (adapters/slack/crypto.py)

def encrypt_token(plaintext: str, key: bytes) -> EncryptedTokenData:
    iv = os.urandom(12)  # 96-bit random IV
    aesgcm = AESGCM(key)
    ct_with_tag = aesgcm.encrypt(iv, plaintext.encode(), None)
    ciphertext = ct_with_tag[:-16]  # everything except last 16 bytes
    tag = ct_with_tag[-16:]         # 128-bit auth tag
    return EncryptedTokenData(
        iv=base64.b64encode(iv),
        data=base64.b64encode(ciphertext),
        tag=base64.b64encode(tag),
    )
  • Algorithm: AES-256-GCM (authenticated encryption)
  • IV: 12 bytes from os.urandom() (CSPRNG)
  • Auth tag: 16 bytes (128-bit)
  • Key format: 32-byte key, accepted as 64-char hex or 44-char base64
  • Library: cryptography (via the crypto extra)
  • Lazy import: cryptography is imported inside the encrypt/decrypt functions, not at module level

Key Management

The encryption key is provided via the encryption_key config option on the Slack adapter. It is the deployer's responsibility to:

  1. Generate a strong 256-bit key (python -c "import secrets; print(secrets.token_hex(32))")
  2. Store it securely (environment variable, secrets manager)
  3. Rotate it by re-encrypting stored tokens with the new key

Timing-Safe HMAC Comparisons

All webhook signature verifications use hmac.compare_digest() for constant-time comparison. This prevents timing side-channel attacks where an attacker could determine the correct signature byte-by-byte by measuring response times.

This applies to:

  • Slack signing secret verification
  • GitHub webhook signature verification
  • WhatsApp webhook signature verification
  • Linear webhook signature verification
  • Telegram secret token verification
  • Lock token comparison in state adapters

Lock Token Generation

Lock tokens serve as proof of ownership. A holder must present the correct token to release or extend a lock.

  • Generator: secrets.token_hex(16) -- 128 bits of cryptographic randomness
  • Format: {backend}_{timestamp_ms}_{hex} (e.g., mem_1700000000000_a1b2c3d4...)
  • Why CSPRNG: If tokens were predictable, a malicious actor (or a bug in a concurrent process) could forge a token and release someone else's lock, causing data races.

Known Limitations

Telegram: Explicitly Unverified Webhooks

Telegram itself does not require webhook verification, but the adapter now fails closed: without secret_token it refuses webhook mode unless allow_unverified_webhooks=True (or TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS=true) is set, and logs a one-time warning when that opt-out is in effect.

Risk: With the opt-out enabled, anyone who discovers the webhook URL can send fake updates.

Mitigation: Always configure secret_token in production; reserve the opt-out for local development or deployments that authenticate the request upstream of the adapter.

Teams: JWKS Key Rotation Window

The Teams adapter caches JWKS (JSON Web Key Set) public keys to avoid fetching them on every request. When Microsoft rotates its signing keys, there is a window where:

  1. The old key is still in the cache
  2. Microsoft starts signing with the new key
  3. Verification fails until the cache expires and new keys are fetched

Risk: Legitimate webhooks may be rejected during key rotation (~minutes).

Mitigation: The cache TTL is set to 24 hours by default. If verification fails with a cached key, the adapter should (but does not currently) attempt a cache refresh before rejecting the request. This is a known improvement area.

No Input Sanitization on Card Content

Card element content (titles, text, button labels) is passed through to platform APIs without HTML escaping. Each platform's API is responsible for sanitizing output. This is intentional -- double-escaping would produce visible escape sequences.

Risk: If a platform API has an XSS vulnerability, malicious card content could exploit it.

Mitigation: Trust platform APIs to handle their own output encoding. Do not pass user-controlled input directly into card content without application-level sanitization.

What to Audit Before Production Deployment

  1. Webhook secrets are configured for every adapter in use. Never deploy with empty or default secrets.

  2. Encryption key is set for Slack multi-workspace OAuth if bot tokens are persisted (Redis/Postgres state backends).

  3. Telegram secret_token is set (required in webhook mode) and allow_unverified_webhooks / TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS is not enabled. With the opt-out, anyone can submit fake updates to your webhook endpoint.

  4. State backend is production-grade. MemoryStateAdapter emits a warning in production environments but does not prevent usage. Use Redis or PostgreSQL.

  5. HTTPS is enforced on all webhook endpoints. Webhook signatures are useless if the request body can be observed and replayed over HTTP.

  6. Rate limiting is configured at the HTTP layer (reverse proxy / load balancer). The SDK raises RateLimitError / AdapterRateLimitError when platform rate limits are hit, but does not implement client-side rate limiting.

  7. Dependency audit: Run pip audit or equivalent to check for known vulnerabilities in dependencies (cryptography, slack-sdk, pynacl, aiohttp, etc.).

  8. Log level: Set to info or warn in production. debug level logs message content and adapter payloads, which may contain sensitive data.

There aren't any published security advisories