Skip to content

Commit 17740da

Browse files
fix: address @jenn-newton comments
- Add authentication and origin validation middleware to /config endpoint - Implement timing-safe token comparison using crypto.timingSafeEqual() - Add config read to initial env setup s.t. /config call is authenticated
1 parent ab062d8 commit 17740da

File tree

2 files changed

+37
-7
lines changed

2 files changed

+37
-7
lines changed

client/src/App.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import ToolsTab from "./components/ToolsTab";
6363
import { InspectorConfig } from "./lib/configurationTypes";
6464
import {
6565
getMCPProxyAddress,
66+
getMCPProxyAuthToken,
6667
getInitialSseUrl,
6768
getInitialTransportType,
6869
getInitialCommand,
@@ -345,7 +346,13 @@ const App = () => {
345346
}, [sseUrl]);
346347

347348
useEffect(() => {
348-
fetch(`${getMCPProxyAddress(config)}/config`)
349+
const headers: HeadersInit = {};
350+
const proxyAuthToken = getMCPProxyAuthToken(config);
351+
if (proxyAuthToken) {
352+
headers['Authorization'] = `Bearer ${proxyAuthToken}`;
353+
}
354+
355+
fetch(`${getMCPProxyAddress(config)}/config`, { headers })
349356
.then((response) => response.json())
350357
.then((data) => {
351358
setEnv(data.defaultEnvironment);
@@ -359,8 +366,7 @@ const App = () => {
359366
.catch((error) =>
360367
console.error("Error fetching default environment:", error),
361368
);
362-
// eslint-disable-next-line react-hooks/exhaustive-deps
363-
}, []);
369+
}, [config]);
364370

365371
useEffect(() => {
366372
rootsRef.current = roots;

server/src/index.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
1919
import express from "express";
2020
import { findActualExecutable } from "spawn-rx";
2121
import mcpProxy from "./mcpProxy.js";
22-
import { randomUUID, randomBytes } from "node:crypto";
22+
import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
2323

2424
const SSE_HEADERS_PASSTHROUGH = ["authorization"];
2525
const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
@@ -120,14 +120,38 @@ const authMiddleware = (req: express.Request, res: express.Response, next: expre
120120
return next();
121121
}
122122

123-
const authHeader = req.headers.authorization;
124-
if (!authHeader || authHeader !== `Bearer ${sessionToken}`) {
123+
const sendUnauthorized = () => {
125124
res.status(401).json({
126125
error: "Unauthorized",
127126
message: "Authentication required. Use the session token shown in the console when starting the server."
128127
});
128+
};
129+
130+
const authHeader = req.headers.authorization;
131+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
132+
sendUnauthorized();
129133
return;
130134
}
135+
136+
const providedToken = authHeader.substring(7); // Remove 'Bearer ' prefix
137+
const expectedToken = sessionToken;
138+
139+
// Convert to buffers for timing-safe comparison
140+
const providedBuffer = Buffer.from(providedToken);
141+
const expectedBuffer = Buffer.from(expectedToken);
142+
143+
// Check length first to prevent timing attacks
144+
if (providedBuffer.length !== expectedBuffer.length) {
145+
sendUnauthorized();
146+
return;
147+
}
148+
149+
// Perform timing-safe comparison
150+
if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
151+
sendUnauthorized();
152+
return;
153+
}
154+
131155
next();
132156
};
133157

@@ -444,7 +468,7 @@ app.get("/health", (req, res) => {
444468
});
445469
});
446470

447-
app.get("/config", (req, res) => {
471+
app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
448472
try {
449473
res.json({
450474
defaultEnvironment,

0 commit comments

Comments
 (0)