mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user