VYPR
Moderate severityOSV Advisory· Published Feb 2, 2026· Updated Feb 3, 2026

Khoj has an IDOR in Notion OAuth Flow Enables Index Poisoning

CVE-2025-69207

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.

PackageAffected versionsPatched versions
khojPyPI
<= 2.0.0b25.dev3

Affected products

1

Patches

1
1b7ccd141d47

Harden the user check of the Notion integration

https://github.com/khoj-ai/khojDebanjumDec 29, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.