HedgeDoc Security Testing: Default Credentials, API Key, Database, and .env SESSION_SECRET

HedgeDoc (formerly CodiMD) is a widely deployed open-source real-time collaborative markdown editor used for team documentation, meeting notes, and shared writing — it stores collaborative documents that often contain sensitive internal content. Key assessment areas: CMD_SESSION_SECRET signs Express session cookies; guest access is enabled by default allowing unauthenticated note reading; note IDs can be enumerated or discovered through sharing links; PostgreSQL stores all note content including notes marked as private by individual permissions; and OAuth provider tokens are stored in the database. This guide covers systematic HedgeDoc security assessment.

Table of Contents

  1. Authentication Configuration and Guest Access Testing
  2. Note Enumeration and Content Access
  3. SESSION_SECRET and Database Extraction
  4. HedgeDoc Security Hardening

Authentication Configuration and Guest Access Testing

# HedgeDoc — authentication configuration and guest access testing
HEDGEDOC_URL="https://hedgedoc.example.com"

# Check if guest access is enabled (default: true)
# Guests can view and sometimes edit notes without authentication
curl -s "${HEDGEDOC_URL}/config" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('HedgeDoc configuration:')
print(f'  allowAnonymous: {d.get(\"allowAnonymous\")}')       # Guest editing
print(f'  allowAnonymousEdits: {d.get(\"allowAnonymousEdits\")}')
print(f'  allowFreeURL: {d.get(\"allowFreeURL\")}')          # Custom note names
print(f'  requireFreeURLAuthentication: {d.get(\"requireFreeURLAuthentication\")}')
print(f'  Email registration: {d.get(\"emailLogin\")}')
print(f'  GitHub auth: {d.get(\"github\")}')
print(f'  Google auth: {d.get(\"google\")}')
print(f'  LDAP auth: {d.get(\"ldap\")}')
print(f'  Version: {d.get(\"version\")}')
" 2>/dev/null

# Test guest access to existing notes (if allowAnonymous is true)
# Note IDs are typically lowercase hex UUIDs or custom names
curl -s -o /dev/null -w "%{http_code}" "${HEDGEDOC_URL}/{note-id}" 2>/dev/null
# 200 = accessible without authentication

# Try to access the note download API
curl -s "${HEDGEDOC_URL}/{note-id}/download" 2>/dev/null | head -30
# Returns raw markdown if accessible

# Test email/password login
curl -s -X POST "${HEDGEDOC_URL}/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"hedgedoc"}' \
  -D /tmp/hd_headers.txt 2>/dev/null
grep -i "set-cookie\|location" /tmp/hd_headers.txt | head -5

Note Enumeration and Content Access

# HedgeDoc — note enumeration and content access
HEDGEDOC_URL="https://hedgedoc.example.com"

# Enumerate notes by testing common note IDs
# HedgeDoc generates random IDs but team deployments often use memorable custom names

# Test common custom note names (allowFreeURL must be enabled)
for NOTE in "meeting-notes" "team-docs" "onboarding" "roadmap" "architecture" \
            "secrets" "passwords" "credentials" "config" "infrastructure"; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${HEDGEDOC_URL}/${NOTE}" 2>/dev/null)
  [ "$STATUS" = "200" ] && echo "FOUND: ${HEDGEDOC_URL}/${NOTE} (HTTP ${STATUS})"
done

# Download note content via the download endpoint
NOTE_ID="your-note-id"
curl -s "${HEDGEDOC_URL}/${NOTE_ID}/download" 2>/dev/null | head -100
# Returns raw markdown — full note content

# Notes API (requires authentication)
SESSION_COOKIE="connect.sid=your-session-cookie"
curl -s "${HEDGEDOC_URL}/history" \
  -H "Cookie: ${SESSION_COOKIE}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
history = d.get('history',[])
print(f'Note history entries: {len(history)}')
for note in history[:10]:
    print(f'  [{note.get(\"id\")}] {note.get(\"text\",\"\")[:50]} updated={str(note.get(\"updatedAt\",\"\"))[:10]}')
    print(f'    pinned={note.get(\"pinned\")} tags={note.get(\"tags\")}')
" 2>/dev/null

# Get note metadata
curl -s "${HEDGEDOC_URL}/${NOTE_ID}/info" \
  -H "Cookie: ${SESSION_COOKIE}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Title: {d.get(\"title\")}')
print(f'Views: {d.get(\"viewcount\")} Updated: {d.get(\"updatetime\")}')
print(f'Permission: {d.get(\"permission\")}')
" 2>/dev/null

SESSION_SECRET and Database Extraction

# HedgeDoc CMD_SESSION_SECRET and database credential extraction

# .env / config file — HedgeDoc configuration
cat /hedgedoc/config/config.json 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
production = d.get('production',{})
for k,v in production.items():
    if any(keyword in k.lower() for keyword in ['secret','password','db','sql','token','key','ldap']):
        print(f'{k}: {str(v)[:80]}')
" 2>/dev/null

# Docker environment variables
docker inspect hedgedoc 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 ['CMD_SESSION','CMD_DB','CMD_LDAP','CMD_OAUTH','CMD_GITHUB','SECRET','PASSWORD']):
            print(e)
" 2>/dev/null
# CMD_SESSION_SECRET — Express session signing secret
# CMD_DB_URL — PostgreSQL connection string
# CMD_LDAP_* — LDAP server configuration
# CMD_GITHUB_CLIENTID/SECRET — GitHub OAuth credentials

# PostgreSQL — all HedgeDoc data
DB_URL="postgresql://hedgedoc:PASSWORD@localhost/hedgedoc"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All users with OAuth tokens
SELECT u.id, u.email, u.name,
       u.profile,
       u.created_at, u.updated_at
FROM "Users" u
ORDER BY u.created_at ASC
LIMIT 10;
EOF
-- profile JSONB: contains GitHub/Google OAuth access tokens

-- All notes with permissions
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT n.id, n.alias,
       LEFT(n.content, 200) as content_preview,
       n.permission, n.view_count,
       n.created_at, n.updated_at,
       u.email as owner
FROM "Notes" n
LEFT JOIN "Users" u ON n.owner_id = u.id
ORDER BY n.updated_at DESC
LIMIT 20;
EOF
-- permission: free/editable/limited/locked/protected/private
-- content: full markdown text of the note
-- NOTE: 'private' notes are still readable by DB access

HedgeDoc Security Hardening

HedgeDoc Security Hardening Checklist:
Security TestMethodRisk
Guest access note enumeration without authenticationGET /{note-id} — if allowAnonymous is true (default), read note content without login; enumerate common custom note names when allowFreeURL is enabled; collaborative team documents often contain sensitive business contentHigh
CMD_SESSION_SECRET extraction and session forgeryRead .env CMD_SESSION_SECRET — Express session signing key; forge valid session cookies for any HedgeDoc user; full access to all notes visible to that userHigh
PostgreSQL Notes content extractionSELECT content FROM "Notes" — all collaborative documents regardless of permission setting; private notes are not encrypted in the database; complete team documentation libraryHigh
OAuth token extraction from Users profile columnSELECT profile FROM "Users" — JSON containing GitHub/Google OAuth access tokens; enables access to linked accounts' repositories, files, and data beyond HedgeDocHigh
Note history enumeration for authenticated usersGET /history — list of all notes the user has viewed or created; combined with download endpoint access, enables systematic enumeration of all notes the user can accessMedium

Automate HedgeDoc Security Testing

Ironimo tests HedgeDoc deployments for guest access note enumeration without authentication, CMD_SESSION_SECRET session forgery, PostgreSQL Notes table content extraction, OAuth token extraction from Users profile column, free URL note name enumeration for common team document names, note permission bypass for private notes, LDAP authentication attribute injection testing, Docker CMD_SESSION_SECRET/CMD_DB_URL exposure, email login credential brute force, and note share link access boundary testing.

Start free scan