GHSA-6WHJ-7QMG-86QJ
Vulnerability from github – Published: 2026-02-02 17:31 – Updated: 2026-02-03 16:09Summary
An IDOR in the Notion OAuth callback allows an attacker to hijack any user's Notion integration by manipulating the state parameter. The callback endpoint accepts any user UUID without verifying the OAuth flow was initiated by that user, allowing attackers to replace victims' Notion configurations with their own, resulting in data poisoning and unauthorized access to the victim's Khoj search index.
This attack requires knowing the user's UUID which can be leaked through shared conversations where an AI generated image is present.
Details
When users share conversations which contain AI generated images, the file path for the image is constructed using the user's UUID. Knowing this UUID, an attacker is able to intercept the OAuth callback for Notion and replace the state parameter with the other user's UUID and sync notion onto their account.
PoC
The vulnerable line of code exists in src/khoj/routers/notion.py on the callback endpoint.
@notion_router.get("/auth/callback")
async def notion_auth_callback(request: Request, background_tasks: BackgroundTasks):
code = request.query_params.get("code")
state = request.query_params.get("state") # <-- Attacker controlled
if not code or not state:
return Response("Missing code or state", status_code=400)
user: KhojUser = await aget_user_by_uuid(state) # <-- No verification!
await NotionConfig.objects.filter(user=user).adelete() # <-- Deletes victim's config
# ... OAuth token exchange ...
access_token = final_response.get("access_token")
await NotionConfig.objects.acreate(token=access_token, user=user) # <-- Stores attacker's token
To exploit is relatively easy. Once we know the victim's UUID, we simply initiate the Notion sync process on our own account and intercept the callback, replacing the state parameter with the victim's UUID.
Impact
Deletes user's existing Notion sync and replaces it with attacker-controlled Notion. Could allow for index poisoning. I'm not entirely sure what Khoj does with synced files but if it's being passed as context to an LLM then I can imagine there's potential here.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "khoj"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "2.0.0b25.dev3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-69207"
],
"database_specific": {
"cwe_ids": [
"CWE-639",
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-02T17:31:33Z",
"nvd_published_at": "2026-02-02T23:16:01Z",
"severity": "MODERATE"
},
"details": "### Summary\nAn IDOR in the Notion OAuth callback allows an attacker to hijack any user\u0027s Notion integration by manipulating the state parameter. The callback endpoint accepts any user UUID without verifying the OAuth flow was initiated by that user, allowing attackers to replace victims\u0027 Notion configurations with their own, resulting in data poisoning and unauthorized access to the victim\u0027s Khoj search index.\n\nThis attack requires knowing the user\u0027s UUID which can be leaked through shared conversations where an AI generated image is present.\n\n### Details\nWhen users share conversations which contain AI generated images, the file path for the image is constructed using the user\u0027s UUID. Knowing this UUID, an attacker is able to intercept the OAuth callback for Notion and replace the `state` parameter with the other user\u0027s UUID and sync notion onto their account.\n\n### PoC\n\nThe vulnerable line of code exists in `src/khoj/routers/notion.py` on the callback endpoint.\n```python\n@notion_router.get(\"/auth/callback\")\nasync def notion_auth_callback(request: Request, background_tasks: BackgroundTasks):\n code = request.query_params.get(\"code\")\n state = request.query_params.get(\"state\") # \u003c-- Attacker controlled\n if not code or not state:\n return Response(\"Missing code or state\", status_code=400)\n\n user: KhojUser = await aget_user_by_uuid(state) # \u003c-- No verification!\n\n await NotionConfig.objects.filter(user=user).adelete() # \u003c-- Deletes victim\u0027s config\n \n # ... OAuth token exchange ...\n \n access_token = final_response.get(\"access_token\")\n await NotionConfig.objects.acreate(token=access_token, user=user) # \u003c-- Stores attacker\u0027s token\n```\n\nTo exploit is relatively easy. Once we know the victim\u0027s UUID, we simply initiate the Notion sync process on our own account and intercept the callback, replacing the `state` parameter with the victim\u0027s UUID.\n\n### Impact\nDeletes user\u0027s existing Notion sync and replaces it with attacker-controlled Notion. Could allow for index poisoning. I\u0027m not entirely sure what Khoj does with synced files but if it\u0027s being passed as context to an LLM then I can imagine there\u0027s potential here.",
"id": "GHSA-6whj-7qmg-86qj",
"modified": "2026-02-03T16:09:54Z",
"published": "2026-02-02T17:31:33Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/khoj-ai/khoj/security/advisories/GHSA-6whj-7qmg-86qj"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-69207"
},
{
"type": "WEB",
"url": "https://github.com/khoj-ai/khoj/commit/1b7ccd141d47f365edeccc57d7316cb0913d748b"
},
{
"type": "PACKAGE",
"url": "https://github.com/khoj-ai/khoj"
},
{
"type": "WEB",
"url": "https://github.com/khoj-ai/khoj/releases/tag/2.0.0-beta.23"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "Khoj has an IDOR in Notion OAuth Flow that Enables Index Poisoning"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.