From 756d9fccf35221ac63a238a675d218447a2c41d1 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 12 Jun 2026 10:20:04 +0900 Subject: [PATCH] =?UTF-8?q?fix(co-gahusb):=20DNS-rebinding=20=EB=B3=B4?= =?UTF-8?q?=ED=98=B8=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20(public=20Ho?= =?UTF-8?q?st=20421=20=ED=95=B4=EA=B2=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FastMCP가 기본 host(127.0.0.1)에서 DNS rebinding 보호를 자동 활성화 → allowed_hosts=localhost만 허용 → nginx가 넘기는 Host gahusb.synology.me가 421. - 실 보안은 nginx 앞단 Bearer 인증(MCP 도달 전 401)이므로 Host 검증 비활성화. - 재현/회귀 테스트 추가 + config.CO_BUS_KEY import-순서 격리 버그 수정 (23 통과). Co-Authored-By: Claude Opus 4.8 (1M context) --- co-gahusb/app/server.py | 8 +++++++- co-gahusb/tests/test_server.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/co-gahusb/app/server.py b/co-gahusb/app/server.py index 3a95187..d787dea 100644 --- a/co-gahusb/app/server.py +++ b/co-gahusb/app/server.py @@ -3,6 +3,7 @@ import logging import redis.asyncio as aioredis from mcp.server.fastmcp import FastMCP +from mcp.server.transport_security import TransportSecuritySettings from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.middleware.base import BaseHTTPMiddleware @@ -16,7 +17,12 @@ _auth_failed_logged = False _redis = aioredis.from_url(config.REDIS_URL, decode_responses=True) -mcp = FastMCP("co-gahusb") +# DNS-rebinding 보호 비활성화: 실 보안은 nginx 앞단 Bearer 인증(MCP 도달 전 401)이다. +# 원격 HTTPS + 정적키 모델이라 Host 화이트리스트는 보안가치 ~0이고, 도메인 변경 시 또 깨진다. +mcp = FastMCP( + "co-gahusb", + transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False), +) # ---- 메시지 ---- diff --git a/co-gahusb/tests/test_server.py b/co-gahusb/tests/test_server.py index 2bbdb1f..b0e8ed2 100644 --- a/co-gahusb/tests/test_server.py +++ b/co-gahusb/tests/test_server.py @@ -2,6 +2,11 @@ import os os.environ["CO_BUS_KEY"] = "test-key" +# config.CO_BUS_KEY는 import 시점에 한 번 읽히므로, 다른 테스트 모듈이 app.config를 +# 먼저 import하면 빈 값으로 굳는다. import 순서와 무관하게 모듈 속성을 직접 강제한다. +from app import config +config.CO_BUS_KEY = "test-key" + from starlette.testclient import TestClient from app.server import app @@ -23,3 +28,27 @@ def test_mcp_wrong_key_rejected(): client = TestClient(app) res = client.post("/mcp", json={}, headers={"Authorization": "Bearer wrong"}) assert res.status_code == 401 + + +def test_mcp_valid_auth_passes_dns_host_check(): + # 유효한 키는 인증 게이트를 통과하고, MCP DNS-rebinding Host 검증에 막혀선 안 된다. + # TestClient 기본 Host="testserver"는 localhost가 아니므로, 보호가 켜져 있으면 421. + # 컨텍스트 매니저로 써야 lifespan(세션 매니저 task group)이 기동되어 MCP 핸들러까지 도달. + with TestClient(app) as client: + res = client.post( + "/mcp", + headers={ + "Authorization": "Bearer test-key", + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }, + json={ + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", "capabilities": {}, + "clientInfo": {"name": "smoke", "version": "0"}, + }, + }, + ) + assert res.status_code != 401 # 인증 통과 + assert res.status_code != 421 # Host 검증에 막히면 안 됨