fix(security): constrain API token chat authority

This commit is contained in:
RaresKeY
2026-08-28 13:25:13 +00:00
parent c9dd68d890
commit aeff329c05
9 changed files with 235 additions and 18 deletions
+36 -5
View File
@@ -40,7 +40,12 @@ from src.foreground_model_routing import (
from src.session_search import search_session_messages
from src.prompt_security import untrusted_context_message
from core.exceptions import SessionNotFoundError
from src.auth_helpers import effective_user, get_current_user
from src.auth_helpers import (
effective_user,
enforce_api_token_chat_controls,
get_current_user,
require_chat_scope,
)
from routes.session_routes import _verify_session_owner
from routes.document_helpers import _owner_session_filter
from core.database import SessionLocal, get_session_mode, set_session_mode
@@ -113,6 +118,7 @@ def _mark_tool_approval_resolved(sess, approval_id: Any, decision: Any) -> bool:
if str(ask_user.get("approval_id") or "") != approval_key:
continue
ask_user["resolved"] = normalized_decision
ask_user["approved_by_interactive_session"] = True
message_id = metadata.get("_db_id")
resolved_metadata = {
key: value for key, value in metadata.items() if key != "_db_id"
@@ -737,6 +743,7 @@ def setup_chat_routes(
# ------------------------------------------------------------------ #
@router.post("/api/chat", response_model=Dict[str, Any])
async def chat_endpoint(request: Request, chat_request: ChatRequest) -> Dict[str, Any]:
require_chat_scope(request)
_set_user_time_from_request(request)
message = chat_request.message
@@ -776,7 +783,10 @@ def setup_chat_routes(
_enforce_chat_privileges(request, sess)
tool_policy = build_effective_tool_policy(last_user_message=message)
allow_tool_preprocessing = not tool_policy.block_all_tool_calls
allow_tool_preprocessing = (
not getattr(request.state, "api_token", False)
and not tool_policy.block_all_tool_calls
)
# Inline memory command
memory_response = None
@@ -927,6 +937,7 @@ def setup_chat_routes(
# ------------------------------------------------------------------ #
@router.post("/api/chat_stream")
async def chat_stream(request: Request) -> StreamingResponse:
require_chat_scope(request)
body = None
try:
if request.headers.get("content-type", "").startswith("application/json"):
@@ -964,7 +975,7 @@ def setup_chat_routes(
compare_mode = str(form_data.get("compare_mode", "")).lower() == "true"
incognito = str(form_data.get("incognito", "")).lower() == "true"
plan_mode = str(form_data.get("plan_mode") or (body or {}).get("plan_mode") or "").lower() == "true"
chat_mode = str(form_data.get("mode", "")).lower() # 'chat' or 'agent'
chat_mode = str(form_data.get("mode") or (body or {}).get("mode") or "chat").lower()
tool_approval_id = (
form_data.get("tool_approval_id")
or (body or {}).get("tool_approval_id")
@@ -978,6 +989,13 @@ def setup_chat_routes(
retired_tool_approval_taint = False
external_untrusted_context_seen = False
tool_approval_continuation = False
api_token_request = enforce_api_token_chat_controls(
request,
mode=chat_mode,
plan_mode=plan_mode,
approval_id=tool_approval_id,
allow_bash=allow_bash,
)
# Workspace: confine the agent's file/shell tools to this folder.
workspace, workspace_rejected = _resolve_request_workspace(
request, form_data.get("workspace")
@@ -1263,6 +1281,16 @@ def setup_chat_routes(
except (ValueError, ValidationError):
raise HTTPException(400, "Invalid request parameters")
# API tokens are integration credentials, not interactive humans. They
# may stream ordinary chat responses, but intent detection, contextual
# follow-ups, and workspace parsing must never promote them into the
# agent/research execution branch.
if api_token_request:
chat_mode = "chat"
auto_escalated = False
workspace = None
use_research = "false"
# ------------------------------------------------------------------ #
# Privilege gates that must fire BEFORE any LLM work / token spend.
# 1. allowed_models — reject if session.model isn't in the user's
@@ -1284,7 +1312,7 @@ def setup_chat_routes(
not tool_approval_continuation
and str(use_research).lower() == "true"
)
if not do_research and not tool_approval_continuation:
if not api_token_request and not do_research and not tool_approval_continuation:
if get_session_mode(session) == 'research_pending':
do_research = True
logger.info(f"Session {session} in research_pending — auto-triggering research")
@@ -1311,7 +1339,10 @@ def setup_chat_routes(
pre_context_tool_policy = build_effective_tool_policy(
last_user_message=message,
)
allow_tool_preprocessing = not pre_context_tool_policy.block_all_tool_calls
allow_tool_preprocessing = (
not api_token_request
and not pre_context_tool_policy.block_all_tool_calls
)
foreground_policy = resolve_foreground_model_policy(
owner=owner,
allowed_models=_allowed_models_for_request(request),
+6 -5
View File
@@ -6,11 +6,12 @@ import logging
import re
from typing import Dict, Any, Optional
from fastapi import APIRouter, Request, HTTPException
from fastapi import APIRouter, Depends, Request, HTTPException
from core.models import ChatMessage
from core.database import SessionLocal, ChatMessage as DbChatMessage, Session as DbSession
from src.auth_helpers import effective_user
from src.auth_helpers import effective_user, require_chat_scope
from src.message_metadata import sanitize_client_message_metadata
from src.topic_analyzer import analyze_topics
from src.upload_handler import reserve_message_upload_references
from routes.session_routes import (
@@ -101,7 +102,7 @@ def _merge_continue_rows_to_delete(db_messages, db1, db2):
def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
router = APIRouter(tags=["history"])
router = APIRouter(tags=["history"], dependencies=[Depends(require_chat_scope)])
def _reserve_message_uploads(
request: Request,
@@ -268,7 +269,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
content = body.get("content", "")
if not content:
raise HTTPException(400, "content is required")
metadata = body.get("metadata")
metadata = sanitize_client_message_metadata(body.get("metadata"))
_reserve_message_uploads(request, content, metadata)
msg = ChatMessage(role=role, content=content, metadata=metadata)
session_manager.add_message(session_id, msg)
@@ -455,7 +456,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
_verify_session_owner(request, session_id)
try:
body = await request.json()
meta_update = body.get("metadata", {})
meta_update = sanitize_client_message_metadata(body.get("metadata", {}))
session = session_manager.get_session(session_id)
# Update in-memory
+14 -5
View File
@@ -4,14 +4,15 @@ import html
import json
import uuid
from datetime import datetime
from fastapi import APIRouter, Form, HTTPException, Response, Request
from fastapi import APIRouter, Depends, Form, HTTPException, Response, Request
import logging
from core.session_manager import SessionManager
from core.models import ChatMessage
from src.request_models import SessionResponse
from core.database import Session as DbSession, SessionLocal, Document, GalleryImage, utcnow_naive
from src.auth_helpers import effective_user, _auth_disabled, owner_filter
from src.auth_helpers import effective_user, _auth_disabled, owner_filter, require_chat_scope
from src.message_metadata import sanitize_client_message_metadata
from src.session_image_cleanup import _generated_image_path_for_cleanup, session_image_refs
from src.session_actions import is_session_recently_active
from src.upload_handler import reserve_message_upload_references
@@ -124,7 +125,11 @@ def _verify_session_owner(request: Request, session_id: str, session_manager=Non
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["sessions"])
router = APIRouter(
prefix="/api",
tags=["sessions"],
dependencies=[Depends(require_chat_scope)],
)
def _current_user_is_admin(request: Request, user: str | None) -> bool:
if not user:
@@ -554,7 +559,7 @@ def setup_session_routes(
upload_handler,
owner,
message.get("content"),
message.get("metadata"),
sanitize_client_message_metadata(message.get("metadata")),
)
if missing_id:
raise HTTPException(
@@ -564,7 +569,11 @@ def setup_session_routes(
except (AttributeError, TypeError, ValueError) as exc:
raise HTTPException(400, "Invalid message attachment metadata") from exc
for m in messages:
sess.add_message(ChatMessage(m["role"], m["content"], metadata=m.get("metadata")))
sess.add_message(ChatMessage(
m["role"],
m["content"],
metadata=sanitize_client_message_metadata(m.get("metadata")),
))
session_manager.save_sessions()
return {"ok": True, "count": len(messages)}
+7 -3
View File
@@ -6,7 +6,7 @@ import asyncio
import shutil
import uuid
from pathlib import Path
from fastapi import APIRouter, Request, File, UploadFile, HTTPException, Form
from fastapi import APIRouter, Depends, Request, File, UploadFile, HTTPException, Form
from typing import List, Optional
import logging
from core.middleware import require_admin
@@ -21,7 +21,7 @@ from core.database import (
Note,
Session as DbSession,
)
from src.auth_helpers import effective_user
from src.auth_helpers import effective_user, require_chat_scope
from src.attachment_refs import attachment_refs_from_metadata
from src.constants import GENERATED_IMAGES_DIR
from src.upload_handler import (
@@ -32,7 +32,11 @@ from src.upload_handler import (
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/upload", tags=["upload"])
router = APIRouter(
prefix="/api/upload",
tags=["upload"],
dependencies=[Depends(require_chat_scope)],
)
UPLOAD_RESPONSE_HEADERS = {"X-Content-Type-Options": "nosniff"}
def _upload_ids_from_persisted_text(value: object) -> set[str]: