Khoj has an IDOR in Notion OAuth Flow Enables Index Poisoning
Description
Khoj is a self-hostable artificial intelligence app. Prior to 2.0.0-beta.23, 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. This vulnerability is fixed in 2.0.0-beta.23.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
khojPyPI | <= 2.0.0b25.dev3 | — |
Affected products
1Patches
11b7ccd141d47Harden the user check of the Notion integration
1 file changed · +9 −5
src/khoj/routers/notion.py+9 −5 modified@@ -7,9 +7,9 @@ import requests from fastapi import APIRouter, BackgroundTasks, Request, Response +from starlette.authentication import requires from starlette.responses import RedirectResponse -from khoj.database.adapters import aget_user_by_uuid from khoj.database.models import KhojUser, NotionConfig from khoj.routers.helpers import configure_content from khoj.utils.state import SearchType @@ -31,18 +31,22 @@ async def run_in_executor(func, *args): @notion_router.get("/auth/callback") +@requires(["authenticated"], redirect="login_page") async def notion_auth_callback(request: Request, background_tasks: BackgroundTasks): code = request.query_params.get("code") state = request.query_params.get("state") if not code or not state: return Response("Missing code or state", status_code=400) - user: KhojUser = await aget_user_by_uuid(state) + # Use authenticated user from session instead of trusting the state parameter + user: KhojUser = request.user.object - await NotionConfig.objects.filter(user=user).adelete() + # Verify state matches user UUID as CSRF protection + if state != str(user.uuid): + logger.warning(f"Notion OAuth state mismatch for user {user.uuid}") + return Response("Invalid state parameter", status_code=400) - if not user: - raise Exception("User not found") + await NotionConfig.objects.filter(user=user).adelete() bearer_token = f"{NOTION_OAUTH_CLIENT_ID}:{NOTION_OAUTH_CLIENT_SECRET}" base64_encoded_token = base64.b64encode(bearer_token.encode()).decode()
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-6whj-7qmg-86qjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-69207ghsaADVISORY
- github.com/khoj-ai/khoj/commit/1b7ccd141d47f365edeccc57d7316cb0913d748bghsax_refsource_MISCWEB
- github.com/khoj-ai/khoj/releases/tag/2.0.0-beta.23ghsax_refsource_MISCWEB
- github.com/khoj-ai/khoj/security/advisories/GHSA-6whj-7qmg-86qjghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.