Skip to content

Commit 5c91876

Browse files
Theodor N. EngøyTheodor N. Engøy
authored andcommitted
examples: restrict demo CORS to loopback
1 parent db9089b commit 5c91876

4 files changed

Lines changed: 99 additions & 20 deletions

File tree

examples/server/src/elicitationUrlExample.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,32 @@ const AUTH_PORT = process.env.MCP_AUTH_PORT ? Number.parseInt(process.env.MCP_AU
221221

222222
const app = createMcpExpressApp({ host: MCP_HOST });
223223

224-
// Allow CORS all domains, expose the Mcp-Session-Id header
224+
const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/;
225+
226+
let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX;
227+
if (process.env.MCP_CORS_ORIGIN_REGEX) {
228+
try {
229+
corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX);
230+
} catch (error) {
231+
const msg =
232+
error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error);
233+
console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`);
234+
corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX;
235+
}
236+
}
237+
238+
// CORS: allow only loopback origins by default (typical for local dev / Inspector direct connect).
239+
// If you intentionally expose this demo remotely, set MCP_CORS_ORIGIN_REGEX explicitly.
240+
// Also expose the Mcp-Session-Id header.
225241
app.use(
226242
cors({
227-
origin: '*', // Allow all origins
243+
origin: (origin, cb) => {
244+
// Allow non-browser clients (no Origin header).
245+
if (!origin) return cb(null, true);
246+
return cb(null, corsOriginRegex.test(origin));
247+
},
228248
exposedHeaders: ['Mcp-Session-Id'],
229-
credentials: true // Allow cookies to be sent cross-origin
249+
credentials: true
230250
})
231251
);
232252

examples/server/src/honoWebStandardStreamableHttp.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,29 @@ const transport = new WebStandardStreamableHTTPServerTransport();
4141
// Create the Hono app
4242
const app = new Hono();
4343

44-
// Enable CORS for all origins
44+
const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/;
45+
46+
let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX;
47+
if (process.env.MCP_CORS_ORIGIN_REGEX) {
48+
try {
49+
corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX);
50+
} catch (error) {
51+
const msg =
52+
error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error);
53+
console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`);
54+
corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX;
55+
}
56+
}
57+
58+
// CORS: allow only loopback origins by default (typical for local dev / Inspector direct connect).
59+
// If you intentionally expose this demo remotely, set MCP_CORS_ORIGIN_REGEX explicitly.
4560
app.use(
4661
'*',
4762
cors({
48-
origin: '*',
63+
origin: (origin, _c) => {
64+
if (!origin) return null;
65+
return corsOriginRegex.test(origin) ? origin : null;
66+
},
4967
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
5068
allowHeaders: ['Content-Type', 'mcp-session-id', 'Last-Event-ID', 'mcp-protocol-version'],
5169
exposeHeaders: ['mcp-session-id', 'mcp-protocol-version']

examples/server/src/simpleStreamableHttp.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -503,13 +503,31 @@ const AUTH_PORT = process.env.MCP_AUTH_PORT ? Number.parseInt(process.env.MCP_AU
503503

504504
const app = createMcpExpressApp({ host: MCP_HOST });
505505

506-
// Enable CORS for browser-based clients (demo only)
507-
// This allows cross-origin requests and exposes WWW-Authenticate header for OAuth
508-
// WARNING: This configuration is for demo purposes only. In production, you should restrict this to specific origins and configure CORS yourself.
506+
const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/;
507+
508+
let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX;
509+
if (process.env.MCP_CORS_ORIGIN_REGEX) {
510+
try {
511+
corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX);
512+
} catch (error) {
513+
const msg =
514+
error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error);
515+
console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`);
516+
corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX;
517+
}
518+
}
519+
520+
// CORS: allow only loopback origins by default (typical for local dev / Inspector direct connect).
521+
// If you intentionally expose this demo remotely, set MCP_CORS_ORIGIN_REGEX explicitly.
522+
// This also exposes WWW-Authenticate for OAuth flows.
509523
app.use(
510524
cors({
511525
exposedHeaders: ['WWW-Authenticate', 'Mcp-Session-Id', 'Last-Event-Id', 'Mcp-Protocol-Version'],
512-
origin: '*' // WARNING: This allows all origins to access the MCP server. In production, you should restrict this to specific origins.
526+
origin: (origin, cb) => {
527+
// Allow non-browser clients (no Origin header).
528+
if (!origin) return cb(null, true);
529+
return cb(null, corsOriginRegex.test(origin));
530+
}
513531
})
514532
);
515533

examples/shared/src/authServer.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@ export interface SetupAuthServerOptions {
3838
let globalAuth: DemoAuth | null = null;
3939
let demoUserCreated = false;
4040

41+
const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/;
42+
43+
function buildCorsMiddleware(): ReturnType<typeof cors> {
44+
let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX;
45+
if (process.env.MCP_CORS_ORIGIN_REGEX) {
46+
try {
47+
corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX);
48+
} catch (error) {
49+
const msg =
50+
error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error);
51+
console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`);
52+
corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX;
53+
}
54+
}
55+
56+
return cors({
57+
origin: (origin, cb) => {
58+
// Allow non-browser clients (no Origin header).
59+
if (!origin) return cb(null, true);
60+
return cb(null, corsOriginRegex.test(origin));
61+
}
62+
});
63+
}
64+
4165
/**
4266
* Gets the global auth instance (must call setupAuthServer first)
4367
*/
@@ -102,13 +126,11 @@ export function setupAuthServer(options: SetupAuthServerOptions): void {
102126
// Create Express app for auth server
103127
const authApp = express();
104128

105-
// Enable CORS for all origins (demo only) - must be before other middleware
106-
// WARNING: This configuration is for demo purposes only. In production, you should restrict this to specific origins and configure CORS yourself.
107-
authApp.use(
108-
cors({
109-
origin: '*' // WARNING: This allows all origins to access the auth server. In production, you should restrict this to specific origins.
110-
})
111-
);
129+
// CORS: allow only loopback origins by default (typical for local dev / Inspector direct connect).
130+
// If you intentionally expose this demo remotely, set MCP_CORS_ORIGIN_REGEX explicitly.
131+
// Must be before other middleware.
132+
const corsMw = buildCorsMiddleware();
133+
authApp.use(corsMw);
112134

113135
// Create better-auth handler
114136
// toNodeHandler bypasses Express methods
@@ -163,8 +185,8 @@ export function setupAuthServer(options: SetupAuthServerOptions): void {
163185

164186
// OAuth metadata endpoints using better-auth's built-in handlers
165187
// Add explicit OPTIONS handler for CORS preflight
166-
authApp.options('/.well-known/oauth-authorization-server', cors());
167-
authApp.get('/.well-known/oauth-authorization-server', cors(), toNodeHandler(oAuthDiscoveryMetadata(auth)));
188+
authApp.options('/.well-known/oauth-authorization-server', corsMw);
189+
authApp.get('/.well-known/oauth-authorization-server', corsMw, toNodeHandler(oAuthDiscoveryMetadata(auth)));
168190

169191
// Body parsers for non-better-auth routes (like /sign-in)
170192
const maxBodyBytes = 100 * 1024; // Make the default explicit to avoid accidental large-body DoS.
@@ -273,14 +295,15 @@ export function setupAuthServer(options: SetupAuthServerOptions): void {
273295
export function createProtectedResourceMetadataRouter(resourcePath = '/mcp'): Router {
274296
const auth = getAuth();
275297
const router = express.Router();
298+
const corsMw = buildCorsMiddleware();
276299

277300
// Construct the metadata path per RFC 9728 Section 3
278301
const metadataPath = `/.well-known/oauth-protected-resource${resourcePath}`;
279302

280303
// Enable CORS for browser-based clients to discover the auth server
281304
// Add explicit OPTIONS handler for CORS preflight
282-
router.options(metadataPath, cors());
283-
router.get(metadataPath, cors(), toNodeHandler(oAuthProtectedResourceMetadata(auth)));
305+
router.options(metadataPath, corsMw);
306+
router.get(metadataPath, corsMw, toNodeHandler(oAuthProtectedResourceMetadata(auth)));
284307

285308
return router;
286309
}

0 commit comments

Comments
 (0)