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.
# 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
# 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
# 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
| Security Test | Method | Risk |
|---|---|---|
| JWT_SECRET extraction and signer completion link forgery | Read .env JWT_SECRET — forge valid signer JWT tokens; bypass signer email requirement; complete any document signing as any invited signer without email access | Critical |
| API key access to all signer PII and signed documents | GET /api/submissions — all submissions with signer names, emails, phone numbers, IPs; GET /api/submissions/{id}/documents — signed PDF download; full GDPR-regulated data breach | High |
| PostgreSQL submitters table PII extraction | SELECT from submitters — all signer PII including name, email, phone, IP, user agent, signing timestamps; full legal record of all signing events | Critical |
| submission_values field extraction | SELECT from submission_values — all form field responses including signature images, typed names, dates, checkboxes; complete record of what each signer agreed to and entered | High |
| SECRET_KEY_BASE Rails session forgery | Read .env SECRET_KEY_BASE — forge valid Rails session cookies; authenticate as any admin user without knowing credentials | Critical |
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