Fix security vulnerabilities, package-lock.json, misc other changes

- Add rate limiting to prevent DoS attacks on vulnerable routes
- Update package-lock.json to use public npm registry
- Document intentionally permissive CORS for public test server
- Remove health endpoints to match original codebase (no health checks)
- Fix auth server splash page to list all OAuth endpoints
- Update test scripts to check server availability via splash page
This commit is contained in:
Basil Hosmer 2025-10-14 16:42:03 -04:00
commit 38c30a4e70
No known key found for this signature in database
6 changed files with 818 additions and 791 deletions

1529
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -44,8 +44,8 @@ AUTH_MODE=auth_server PORT=3001 BASE_URI=$AUTH_SERVER node dist/index.js &
AUTH_PID=$! AUTH_PID=$!
sleep 5 sleep 5
# Check auth server # Check auth server by accessing splash page
if ! curl -s -f "$AUTH_SERVER/health" > /dev/null; then if ! curl -s -f "$AUTH_SERVER/" > /dev/null 2>&1; then
echo "❌ Auth server failed to start at $AUTH_SERVER" echo "❌ Auth server failed to start at $AUTH_SERVER"
kill $AUTH_PID 2>/dev/null || true kill $AUTH_PID 2>/dev/null || true
exit 1 exit 1
@ -58,8 +58,8 @@ AUTH_MODE=external AUTH_SERVER_URL=$AUTH_SERVER PORT=8080 BASE_URI=$MCP_SERVER n
MCP_PID=$! MCP_PID=$!
sleep 5 sleep 5
# Check MCP server # Check MCP server by accessing splash page
if ! curl -s -f "$MCP_SERVER/health" > /dev/null; then if ! curl -s -f "$MCP_SERVER/" > /dev/null 2>&1; then
echo "❌ MCP server failed to start at $MCP_SERVER" echo "❌ MCP server failed to start at $MCP_SERVER"
kill $AUTH_PID 2>/dev/null || true kill $AUTH_PID 2>/dev/null || true
kill $MCP_PID 2>/dev/null || true kill $MCP_PID 2>/dev/null || true

View file

@ -41,8 +41,8 @@ AUTH_MODE=internal PORT=8080 BASE_URI=$SERVER_URL node dist/index.js &
SERVER_PID=$! SERVER_PID=$!
sleep 5 sleep 5
# Check server health # Check server is running by accessing splash page
if ! curl -s -f "$SERVER_URL/health" > /dev/null; then if ! curl -s -f "$SERVER_URL/" > /dev/null 2>&1; then
echo "❌ Server failed to start at $SERVER_URL" echo "❌ Server failed to start at $SERVER_URL"
kill $SERVER_PID 2>/dev/null || true kill $SERVER_PID 2>/dev/null || true
exit 1 exit 1

View file

@ -15,6 +15,7 @@ import cors from 'cors';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import rateLimit from 'express-rate-limit';
import { config } from './config.js'; import { config } from './config.js';
import { AuthModule } from './modules/auth/index.js'; import { AuthModule } from './modules/auth/index.js';
import { MCPModule } from './modules/mcp/index.js'; import { MCPModule } from './modules/mcp/index.js';
@ -38,6 +39,8 @@ async function main() {
const app = express(); const app = express();
// Basic middleware // Basic middleware
// Intentionally permissive CORS for public MCP reference server
// This allows any MCP client to test against this reference implementation
app.use(cors({ origin: true, credentials: true })); app.use(cors({ origin: true, credentials: true }));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
@ -125,6 +128,7 @@ async function main() {
console.log(` Authorize: GET ${config.baseUri}/authorize`); console.log(` Authorize: GET ${config.baseUri}/authorize`);
console.log(` Get Token: POST ${config.baseUri}/token`); console.log(` Get Token: POST ${config.baseUri}/token`);
console.log(` Introspect: POST ${config.baseUri}/introspect`); console.log(` Introspect: POST ${config.baseUri}/introspect`);
console.log(` Revoke: POST ${config.baseUri}/revoke`);
} else if (config.auth.mode === 'external') { } else if (config.auth.mode === 'external') {
// ======================================== // ========================================
@ -161,12 +165,20 @@ async function main() {
console.log('MCP Endpoints:'); console.log('MCP Endpoints:');
console.log(` Streamable HTTP: ${config.baseUri}/mcp`); console.log(` Streamable HTTP: ${config.baseUri}/mcp`);
console.log(` SSE (legacy): ${config.baseUri}/sse`); console.log(` SSE (legacy): ${config.baseUri}/sse`);
console.log(` Health Check: ${config.baseUri}/health`);
console.log(` OAuth Metadata: ${config.baseUri}/.well-known/oauth-authorization-server`); console.log(` OAuth Metadata: ${config.baseUri}/.well-known/oauth-authorization-server`);
} }
// Rate limiter for splash page (moderate limit)
const splashPageLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 50, // 50 requests per minute
message: 'Too many requests to splash page',
standardHeaders: true,
legacyHeaders: false,
});
// Splash page (customize based on mode) // Splash page (customize based on mode)
app.get('/', (req, res) => { app.get('/', splashPageLimiter, (req, res) => {
if (config.auth.mode === 'auth_server') { if (config.auth.mode === 'auth_server') {
// Simple splash page for standalone auth server // Simple splash page for standalone auth server
res.send(` res.send(`
@ -199,13 +211,14 @@ async function main() {
<div class="endpoint">GET ${config.baseUri}/authorize - Authorization endpoint</div> <div class="endpoint">GET ${config.baseUri}/authorize - Authorization endpoint</div>
<div class="endpoint">POST ${config.baseUri}/token - Token endpoint</div> <div class="endpoint">POST ${config.baseUri}/token - Token endpoint</div>
<div class="endpoint">POST ${config.baseUri}/introspect - Token introspection</div> <div class="endpoint">POST ${config.baseUri}/introspect - Token introspection</div>
<div class="endpoint">POST ${config.baseUri}/revoke - Token revocation</div>
</body> </body>
</html> </html>
`); `);
} else { } else {
const srcStaticDir = path.join(__dirname, 'static'); const srcStaticDir = path.join(__dirname, 'static');
const splashPath = path.join(srcStaticDir, 'index.html'); const splashPath = path.join(srcStaticDir, 'index.html');
let html = fs.readFileSync(splashPath, 'utf8'); const html = fs.readFileSync(splashPath, 'utf8');
res.send(html); res.send(html);
} }
}); });

View file

@ -13,6 +13,7 @@ import { Router } from 'express';
import express from 'express'; import express from 'express';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import rateLimit from 'express-rate-limit';
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'; import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
import { FeatureReferenceAuthProvider } from './auth/provider.js'; import { FeatureReferenceAuthProvider } from './auth/provider.js';
import { handleMockUpstreamAuthorize, handleMockUpstreamCallback } from './handlers/mock-upstream-idp.js'; import { handleMockUpstreamAuthorize, handleMockUpstreamCallback } from './handlers/mock-upstream-idp.js';
@ -73,6 +74,23 @@ export class AuthModule {
private setupRouter(): Router { private setupRouter(): Router {
const router = Router(); const router = Router();
// Rate limiters for different route types
const authLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 20, // 20 requests per minute for auth endpoints
message: 'Too many authentication attempts',
standardHeaders: true,
legacyHeaders: false,
});
const staticAssetLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute for static assets
message: 'Too many requests for static assets',
standardHeaders: true,
legacyHeaders: false,
});
// OAuth endpoints via SDK's mcpAuthRouter // OAuth endpoints via SDK's mcpAuthRouter
router.use(mcpAuthRouter({ router.use(mcpAuthRouter({
provider: this.provider, provider: this.provider,
@ -108,24 +126,15 @@ export class AuthModule {
}); });
// Mock upstream IDP endpoints (for demo purposes) // Mock upstream IDP endpoints (for demo purposes)
router.get('/mock-upstream-idp/authorize', handleMockUpstreamAuthorize); router.get('/mock-upstream-idp/authorize', authLimiter, handleMockUpstreamAuthorize);
router.get('/mock-upstream-idp/callback', handleMockUpstreamCallback); router.get('/mock-upstream-idp/callback', authLimiter, handleMockUpstreamCallback);
// Static assets for auth pages // Static assets for auth pages
router.get('/mcp-logo.png', (req, res) => { router.get('/mcp-logo.png', staticAssetLimiter, (req, res) => {
const logoPath = path.join(__dirname, 'static', 'mcp.png'); const logoPath = path.join(__dirname, 'static', 'mcp.png');
res.sendFile(logoPath); res.sendFile(logoPath);
}); });
// Health check
router.get('/auth/health', (req, res) => {
res.json({
status: 'healthy',
module: 'auth',
mode: 'internal'
});
});
return router; return router;
} }
} }

View file

@ -13,6 +13,7 @@ import { Router, Request, Response, NextFunction } from 'express';
import cors from 'cors'; import cors from 'cors';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import rateLimit from 'express-rate-limit';
import { BearerAuthMiddlewareOptions, requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'; import { BearerAuthMiddlewareOptions, requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js'; import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
import { ITokenValidator } from '../../interfaces/auth-validator.js'; import { ITokenValidator } from '../../interfaces/auth-validator.js';
@ -47,7 +48,18 @@ export class MCPModule {
private setupRouter(): Router { private setupRouter(): Router {
const router = Router(); const router = Router();
// Rate limiter for static assets
const staticAssetLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute for static assets
message: 'Too many requests for static assets',
standardHeaders: true,
legacyHeaders: false,
});
// CORS configuration for MCP endpoints // CORS configuration for MCP endpoints
// Intentionally permissive CORS for public MCP reference server
// This allows any MCP client to test against this reference implementation
const corsOptions = { const corsOptions = {
origin: true, // Allow any origin origin: true, // Allow any origin
methods: ['GET', 'POST', 'DELETE'], methods: ['GET', 'POST', 'DELETE'],
@ -88,20 +100,8 @@ export class MCPModule {
router.get('/sse', cors(corsOptions), bearerAuth, sseHeaders, handleSSEConnection); router.get('/sse', cors(corsOptions), bearerAuth, sseHeaders, handleSSEConnection);
router.post('/message', cors(corsOptions), bearerAuth, securityHeaders, handleMessage); router.post('/message', cors(corsOptions), bearerAuth, securityHeaders, handleMessage);
// Health check
router.get('/health', (req, res) => {
res.json({
status: 'healthy',
service: 'mcp',
endpoints: {
streamable: '/mcp',
sse: '/sse'
}
});
});
// Static files for MCP // Static files for MCP
router.get('/styles.css', (req, res) => { router.get('/styles.css', staticAssetLimiter, (req, res) => {
const cssPath = path.join(__dirname, '../../static', 'styles.css'); const cssPath = path.join(__dirname, '../../static', 'styles.css');
res.setHeader('Content-Type', 'text/css'); res.setHeader('Content-Type', 'text/css');
res.sendFile(cssPath); res.sendFile(cssPath);