mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
Fix authentication bypass vulnerabilities in MCP/SSE endpoints
- Fix validateBearerAuth to use loadOriginalSettings() instead of loadSettings() to prevent bearer auth bypass when no user context exists - Add authentication validation to sseUserContextMiddleware for user-scoped routes to prevent user impersonation via URL path parameters - Require valid OAuth/bearer token for accessing /:user/mcp and /:user/sse endpoints - Return 401 Unauthorized for user-scoped routes without authentication - Return 403 Forbidden when authenticated user doesn't match requested username Security improvements: 1. Bearer auth now correctly reads enableBearerAuth from system config 2. User-scoped endpoints now require authentication 3. Users can only access their own resources 4. Prevents impersonation attacks via URL manipulation Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
This commit is contained in:
@@ -37,6 +37,10 @@ export const userContextMiddleware = async (
|
||||
/**
|
||||
* User context middleware for SSE/MCP endpoints
|
||||
* Extracts user from URL path parameter and sets user context
|
||||
*
|
||||
* SECURITY: For user-scoped routes (/:user/...), this middleware validates
|
||||
* that the user is authenticated via JWT, OAuth, or Bearer token and that
|
||||
* the authenticated user matches the requested username in the URL.
|
||||
*/
|
||||
export const sseUserContextMiddleware = async (
|
||||
req: Request,
|
||||
@@ -60,19 +64,42 @@ export const sseUserContextMiddleware = async (
|
||||
};
|
||||
|
||||
if (username) {
|
||||
// For user-scoped routes, set the user context
|
||||
// Note: In a real implementation, you should validate the user exists
|
||||
// and has proper permissions
|
||||
const user: IUser = {
|
||||
username,
|
||||
password: '',
|
||||
isAdmin: false, // TODO: Should be retrieved from user database
|
||||
};
|
||||
// SECURITY FIX: For user-scoped routes, authenticate the request
|
||||
// and validate that the authenticated user matches the requested username
|
||||
|
||||
// Try to authenticate via Bearer token (OAuth or configured bearer key)
|
||||
const rawAuthHeader = Array.isArray(req.headers.authorization)
|
||||
? req.headers.authorization[0]
|
||||
: req.headers.authorization;
|
||||
const bearerUser = resolveOAuthUserFromAuthHeader(rawAuthHeader);
|
||||
|
||||
userContextService.setCurrentUser(user);
|
||||
attachCleanupHandlers();
|
||||
console.log(`User context set for SSE/MCP endpoint: ${username}`);
|
||||
if (bearerUser) {
|
||||
// Authenticated via OAuth bearer token
|
||||
// Verify the authenticated user matches the requested username
|
||||
if (bearerUser.username !== username) {
|
||||
res.status(403).json({
|
||||
error: 'forbidden',
|
||||
error_description: `Authenticated user '${bearerUser.username}' cannot access resources for user '${username}'`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
userContextService.setCurrentUser(bearerUser);
|
||||
attachCleanupHandlers();
|
||||
console.log(`OAuth user context set for SSE/MCP endpoint: ${bearerUser.username}`);
|
||||
} else {
|
||||
// SECURITY: No valid authentication provided for user-scoped route
|
||||
// User-scoped routes require authentication to prevent impersonation
|
||||
cleanup();
|
||||
res.status(401).json({
|
||||
error: 'unauthorized',
|
||||
error_description: 'Authentication required for user-scoped MCP endpoints. Please provide valid credentials via Authorization header.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Global route (no user in path)
|
||||
// Still check for OAuth bearer authentication if provided
|
||||
const rawAuthHeader = Array.isArray(req.headers.authorization)
|
||||
? req.headers.authorization[0]
|
||||
: req.headers.authorization;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { deleteMcpServer, getMcpServer } from './mcpService.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
import { loadSettings, loadOriginalSettings } from '../config/index.js';
|
||||
import config from '../config/index.js';
|
||||
import { UserContextService } from './userContextService.js';
|
||||
import { RequestContextService } from './requestContextService.js';
|
||||
@@ -31,7 +31,16 @@ type BearerAuthResult =
|
||||
};
|
||||
|
||||
const validateBearerAuth = (req: Request): BearerAuthResult => {
|
||||
const settings = loadSettings();
|
||||
// SECURITY FIX: Use loadOriginalSettings() to bypass user filtering
|
||||
// This ensures enableBearerAuth configuration is always read correctly
|
||||
// and not removed by DataServicex.filterSettings() for unauthenticated users
|
||||
const settings = loadOriginalSettings();
|
||||
|
||||
// Handle case where settings might be undefined (e.g., in tests)
|
||||
if (!settings) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
const routingConfig = settings.systemConfig?.routing || {
|
||||
enableGlobalRoute: true,
|
||||
enableGroupNameRoute: true,
|
||||
|
||||
Reference in New Issue
Block a user