diff --git a/daemon/src/server.ts b/daemon/src/server.ts index 8cfc09e..29db2b3 100644 --- a/daemon/src/server.ts +++ b/daemon/src/server.ts @@ -25,10 +25,15 @@ const app = express(); const PORT = Number(process.env.MCP_DAEMON_PORT ?? 8006); const HOST = process.env.MCP_DAEMON_HOST ?? '127.0.0.1'; +// MCP clients (Claude Desktop, etc.) are external — CORS must be permissive. +// The daemon is already protected by requiring a DreamFactory session token. app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true })); +// Internal API key for PHP proxy -> daemon communication +const INTERNAL_API_KEY = process.env.MCP_INTERNAL_KEY ?? ''; + /** * Send 401 Unauthorized response */ @@ -46,12 +51,12 @@ function sendUnauthorized(res: Response): void { const sessionManager = new SessionService(); const sessions = new Map(); -// Health check endpoints +// Health check endpoints — do not expose session IDs app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: Math.floor(Date.now() / 1000), - sessions: Array.from(sessions.keys()) + active_sessions: sessions.size, }); }); @@ -59,12 +64,15 @@ app.get('/ping', (_req, res) => { res.json({ status: 'ok', timestamp: Math.floor(Date.now() / 1000), - sessions: Array.from(sessions.keys()) + active_sessions: sessions.size, }); }); -// Cache management endpoint +// Cache management endpoint — requires internal API key from PHP proxy app.post('/mcp/cache/clear', (req, res) => { + if (INTERNAL_API_KEY && req.headers['x-mcp-internal-key'] !== INTERNAL_API_KEY) { + return res.status(403).json({ error: 'Forbidden: invalid internal key' }); + } const service = typeof req.body === 'object' ? req.body?.service : undefined; if (service) { for (const [sessionId, entry] of sessions.entries()) { diff --git a/src/Http/Controllers/McpOAuthController.php b/src/Http/Controllers/McpOAuthController.php index ab6502a..09c2be3 100644 --- a/src/Http/Controllers/McpOAuthController.php +++ b/src/Http/Controllers/McpOAuthController.php @@ -221,11 +221,21 @@ public function authorizeGet(Request $request, string $mcpService) ]); } - // Validate redirect URI + // Validate redirect URI is present and registered with the client if (empty($redirectUri)) { return $this->errorResponse('invalid_request', 'Missing redirect_uri'); } + $registeredUris = $client->redirect_uris ?? []; + if (!empty($registeredUris) && !in_array($redirectUri, $registeredUris, true)) { + Log::warning('MCP OAuth: redirect_uri not registered', [ + 'provided' => $redirectUri, + 'registered' => $registeredUris, + 'service' => $mcpService, + ]); + return $this->errorResponse('invalid_request', 'redirect_uri is not registered for this client'); + } + // ============================================================ // SSO CHECK: Try to find existing DreamFactory session // If user is already logged in, skip the login page entirely @@ -931,17 +941,16 @@ private function getExistingDfSession(Request $request): ?array */ private function getBaseUrl(Request $request): string { - $scheme = $request->header('X-Forwarded-Proto', $request->getScheme()); - $host = $request->header('X-Forwarded-Host', $request->getHost()); - - // For proxied requests, don't include port (proxy handles it) - // Only include port for direct requests with non-standard ports - if ($request->header('X-Forwarded-Proto')) { - // Proxied request - don't add port - return "{$scheme}://{$host}"; + // Prefer configured APP_URL to prevent host header injection attacks. + // Only fall back to request headers for local/dev environments. + $configuredUrl = rtrim(config('app.url', ''), '/'); + if (!empty($configuredUrl) && $configuredUrl !== 'http://localhost') { + return $configuredUrl; } - // Direct request - check if we need to include port + $scheme = $request->getScheme(); + $host = $request->getHost(); + $port = $request->getPort(); $url = "{$scheme}://{$host}"; if (($scheme === 'http' && $port != 80) || ($scheme === 'https' && $port != 443)) { diff --git a/src/Http/Middleware/McpStreamMiddleware.php b/src/Http/Middleware/McpStreamMiddleware.php index 1e88f1f..a47f854 100644 --- a/src/Http/Middleware/McpStreamMiddleware.php +++ b/src/Http/Middleware/McpStreamMiddleware.php @@ -45,12 +45,17 @@ class McpStreamMiddleware /** * CORS headers for MCP endpoints */ - private const CORS_HEADERS = [ - 'Access-Control-Allow-Origin' => '*', - 'Access-Control-Allow-Methods' => 'GET, POST, DELETE, OPTIONS', - 'Access-Control-Allow-Headers' => 'Content-Type, Authorization, mcp-session-id', - 'Access-Control-Expose-Headers' => 'WWW-Authenticate', - ]; + // MCP clients (Claude Desktop, etc.) are external — CORS must be permissive. + // Endpoints are protected by requiring a DreamFactory session/Bearer token. + private static function corsHeaders(): array + { + return [ + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Methods' => 'GET, POST, DELETE, OPTIONS', + 'Access-Control-Allow-Headers' => 'Content-Type, Authorization, mcp-session-id', + 'Access-Control-Expose-Headers' => 'WWW-Authenticate', + ]; + } /** * Handle an incoming request. @@ -76,7 +81,7 @@ public function handle(Request $request, Closure $next) // Handle OPTIONS preflight for any MCP path if ($method === 'OPTIONS') { - return response('', 200)->withHeaders(self::CORS_HEADERS); + return response('', 200)->withHeaders(self::corsHeaders()); } $mcpService = $matches[1]; @@ -100,7 +105,7 @@ public function handle(Request $request, Closure $next) // Add CORS headers to response if ($response) { - foreach (self::CORS_HEADERS as $key => $value) { + foreach (self::corsHeaders() as $key => $value) { $response->headers->set($key, $value); } return $response; @@ -116,7 +121,7 @@ public function handle(Request $request, Closure $next) private function handleRfc8414WellKnown(Request $request, Closure $next, string $wellKnownType, string $mcpService, string $method) { if ($method === 'OPTIONS') { - return response('', 200)->withHeaders(self::CORS_HEADERS); + return response('', 200)->withHeaders(self::corsHeaders()); } $controllerMethod = self::RFC8414_WELL_KNOWN[$wellKnownType] ?? null; @@ -129,7 +134,7 @@ private function handleRfc8414WellKnown(Request $request, Closure $next, string $response = $controller->$controllerMethod($request, $mcpService); if ($response) { - foreach (self::CORS_HEADERS as $key => $value) { + foreach (self::corsHeaders() as $key => $value) { $response->headers->set($key, $value); } return $response;