feat(mcp): שער Bearer לתחבורת ה-HTTP — מסרב לעלות בלי טוקן (#231.2)
ב-stdio ההגנה היא הצינור עצמו: רק תהליך שכבר רץ כמשתמש הזה יכול לדבר עם השרת. streamable-http מבטל את התכונה הזו לחלוטין — כל מי שמגיע ל-socket יכול לקרוא לכל אחד מ-108 הכלים, ובמרשם יש case_delete, precedent_library_delete, document_upload וכל כלי כתיבת-הבלוקים. מאזין לא-מאומת הוא, הלכה למעשה, endpoint למחיקת תיקים. מימוש דרך ה-TokenVerifier של ה-SDK ו-BearerAuthBackend שלו — לא middleware משלנו. מסלול-אימות אחד, של המסגרת (G2). ההחלטות שקובעות את בטיחות הפיצ'ר: - **מסרב לעלות בלי טוקן.** MissingTokenError קטלנית. החלופה המפתה — לעלות ולרשום warning — מייצרת מאזין שנראה בריא ועונה על כל קריאה הרסנית. סירוב-אתחול הוא הכשל הבטוח (§6). - הבנייה בזמן-import ולא בתוך main(): FastMCP מקבל token_verifier ו-auth כארגומנטי-בנאי, ולכן טוקן חסר חייב להיכשל *לפני* שה-listener קיים. - **stdio לא נוגע.** דרישת טוקן שם הייתה שוברת כל סשן אינטראקטיבי בלי שום רווח אבטחתי — הגבול שם הוא הצינור. - השוואה בזמן-קבוע (hmac.compare_digest); == נאיבי מדליף את הטוקן בייט-בייט לתוקף שמודד זמנים. - verify_token מחזיר None ולא זורק — זה אות ה"דחייה" של הפרוטוקול ומניב 401 נקי במקום 500 שנקרא ככשל-שרת. - סף אורך 32 תווים; טוקן קצר נדחה באתחול ולא מתגלה מ-access log. - הטוקן נקרא מ-env (שיאוכלס מ-Infisical), לא מוטמע, ולא נרשם ללוג. אומת בהרצה חיה: HTTP בלי טוקן → סירוב לעלות, exit 1 stdio בלי טוקן → עולה כרגיל, auth כבוי POST בלי Authorization → 401 POST עם טוקן שגוי → 401 POST עם הטוקן הנכון → 200 claude דרך HTTP+Bearer → 108 כלים, mcp__legal-ai__case_get מופעי הטוקן בלוג → 0 invariants: G2 — מקיים (מסלול-אימות יחיד, של ה-SDK). G12 — מקיים; המודול נקי מסמלי-פלטפורמה, leak_guard ירוק. INV-AG3 — לא נגוע, השער ירוק. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,11 +52,38 @@ async def lifespan(server: FastMCP) -> AsyncIterator[None]:
|
||||
# ignored and the server binds 8000 regardless (verified 2026-08-05; on this
|
||||
# host 8000 is already taken, so it failed loudly by luck rather than design).
|
||||
#
|
||||
# Default to loopback: this transport is unauthenticated until #231.2 lands and
|
||||
# the registry contains destructive tools.
|
||||
# Default to loopback. The bearer gate below is the real protection; the narrow
|
||||
# bind is defence in depth, not a substitute for it.
|
||||
MCP_HTTP_HOST = os.environ.get("MCP_HTTP_HOST", "127.0.0.1")
|
||||
MCP_HTTP_PORT = int(os.environ.get("MCP_HTTP_PORT", "8790"))
|
||||
|
||||
# Bearer gate — wired only when an HTTP transport is actually selected (#231.2).
|
||||
#
|
||||
# stdio must never require a token: the pipe is the boundary there, and every
|
||||
# interactive session reaches us that way. Demanding a token on stdio would
|
||||
# break all of them for no security gain.
|
||||
#
|
||||
# Building the verifier at import time (rather than inside main()) is deliberate:
|
||||
# FastMCP takes `token_verifier` and `auth` as constructor arguments, so a
|
||||
# missing token has to fail here — before the listener exists — not after it is
|
||||
# already accepting connections.
|
||||
_http_transport = os.environ.get("MCP_TRANSPORT", "stdio").strip() in ("sse", "streamable-http")
|
||||
_auth_kwargs: dict = {}
|
||||
if _http_transport:
|
||||
from mcp.server.auth.settings import AuthSettings
|
||||
|
||||
from legal_mcp.services.http_auth import StaticTokenVerifier, load_token
|
||||
|
||||
_base_url = f"http://{MCP_HTTP_HOST}:{MCP_HTTP_PORT}"
|
||||
_auth_kwargs = {
|
||||
"token_verifier": StaticTokenVerifier(load_token()),
|
||||
# AuthSettings is what switches on the SDK's BearerAuthBackend. We are a
|
||||
# resource server with a pre-shared token, not an OAuth client, so both
|
||||
# URLs simply point at ourselves — they exist to satisfy the protected-
|
||||
# resource metadata contract, and nothing issues tokens from them.
|
||||
"auth": AuthSettings(issuer_url=_base_url, resource_server_url=_base_url),
|
||||
}
|
||||
|
||||
# Create MCP server
|
||||
mcp = FastMCP(
|
||||
"Ezer Mishpati - עוזר משפטי",
|
||||
@@ -64,6 +91,7 @@ mcp = FastMCP(
|
||||
lifespan=lifespan,
|
||||
host=MCP_HTTP_HOST,
|
||||
port=MCP_HTTP_PORT,
|
||||
**_auth_kwargs,
|
||||
)
|
||||
|
||||
# ── Import and register tools ───────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user