Fix: CORS preflight 401'd by AuthMiddleware before CORSMiddleware (#3262)

AuthMiddleware is the outermost middleware, so a credential-less CORS preflight
(OPTIONS + Access-Control-Request-Method) was rejected with 401 before
CORSMiddleware could answer it. That blocks every cross-origin browser/WebView
client: the preflight fails, so the real request is never sent.

Let a genuine preflight through at the top of AuthMiddleware.dispatch via a pure,
unit-tested predicate (core.middleware.is_cors_preflight). Precise -- only
OPTIONS carrying Access-Control-Request-Method; a credentialed request is never
matched -- and no data access.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marius
2026-06-07 14:23:23 +01:00
committed by GitHub
parent a3784da172
commit 04d6a5ccaa
3 changed files with 49 additions and 1 deletions
+10 -1
View File
@@ -54,7 +54,7 @@ from core.constants import (
REQUEST_TIMEOUT, OPENAI_API_KEY, REQUEST_TIMEOUT, OPENAI_API_KEY,
) )
from core.database import SessionLocal, ApiToken from core.database import SessionLocal, ApiToken
from core.middleware import SecurityHeadersMiddleware from core.middleware import SecurityHeadersMiddleware, is_cors_preflight
from core.auth import AuthManager from core.auth import AuthManager
from core.exceptions import ( from core.exceptions import (
SessionNotFoundError, InvalidFileUploadError, SessionNotFoundError, InvalidFileUploadError,
@@ -253,6 +253,15 @@ if AUTH_ENABLED:
class AuthMiddleware(BaseHTTPMiddleware): class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next): async def dispatch(self, request: Request, call_next):
path = request.url.path path = request.url.path
# A genuine CORS preflight (OPTIONS + Access-Control-Request-Method)
# carries no credentials by design and must reach CORSMiddleware to be
# answered. AuthMiddleware is the outermost middleware, so gating the
# preflight on auth 401s it before CORS can respond -- which blocks
# every cross-origin browser/WebView client before the real request
# is sent. Let real preflights through (only OPTIONS w/ the ACRM
# header; never a credentialed request).
if is_cors_preflight(request.method, request.headers):
return await call_next(request)
if _is_auth_exempt(path): if _is_auth_exempt(path):
return await call_next(request) return await call_next(request)
# In-process internal-tool token bypass. Used by the agent # In-process internal-tool token bypass. Used by the agent
+9
View File
@@ -17,6 +17,15 @@ INTERNAL_TOOL_TOKEN = os.environ.get("ODYSSEUS_INTERNAL_TOKEN") or secrets.token
INTERNAL_TOOL_HEADER = "X-Odysseus-Internal-Token" INTERNAL_TOOL_HEADER = "X-Odysseus-Internal-Token"
def is_cors_preflight(method: str, headers) -> bool:
"""True for a genuine CORS preflight: an OPTIONS request carrying the
Access-Control-Request-Method header. Such requests are credential-less by
design and must reach CORSMiddleware to be answered -- gating them on auth
401s the preflight and breaks every cross-origin browser/WebView client.
Pure so it can be unit-tested without standing up the app."""
return method == "OPTIONS" and "access-control-request-method" in headers
def require_admin(request: Request): def require_admin(request: Request):
"""Raise 403 if the current user isn't an admin. """Raise 403 if the current user isn't an admin.
Allows access when auth is explicitly disabled, or when the request carries Allows access when auth is explicitly disabled, or when the request carries
+30
View File
@@ -0,0 +1,30 @@
"""Regression test for the CORS-preflight auth bypass.
AuthMiddleware is the outermost middleware, so it used to 401 the credential-less
OPTIONS preflight before CORSMiddleware could answer it -- which blocks every
cross-origin browser/WebView client before the real request is ever sent. The
fix lets a genuine preflight through; `is_cors_preflight` is the pure predicate
it uses. Guard it so the bypass can't silently regress.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.middleware import is_cors_preflight
def test_genuine_preflight_is_detected():
assert is_cors_preflight("OPTIONS", {"access-control-request-method": "POST"}) is True
def test_bare_options_is_not_a_preflight():
# OPTIONS without Access-Control-Request-Method must NOT bypass auth.
assert is_cors_preflight("OPTIONS", {}) is False
def test_real_methods_are_never_preflight():
headers = {"access-control-request-method": "POST"}
for method in ("GET", "POST", "PUT", "DELETE", "PATCH"):
assert is_cors_preflight(method, headers) is False