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
This commit is contained in:
Felix Weinberger 2025-06-12 17:57:41 +01:00
commit 17740daaf5
No known key found for this signature in database
2 changed files with 37 additions and 7 deletions

View file

@ -63,6 +63,7 @@ import ToolsTab from "./components/ToolsTab";
import { InspectorConfig } from "./lib/configurationTypes"; import { InspectorConfig } from "./lib/configurationTypes";
import { import {
getMCPProxyAddress, getMCPProxyAddress,
getMCPProxyAuthToken,
getInitialSseUrl, getInitialSseUrl,
getInitialTransportType, getInitialTransportType,
getInitialCommand, getInitialCommand,
@ -345,7 +346,13 @@ const App = () => {
}, [sseUrl]); }, [sseUrl]);
useEffect(() => { useEffect(() => {
fetch(`${getMCPProxyAddress(config)}/config`) const headers: HeadersInit = {};
const proxyAuthToken = getMCPProxyAuthToken(config);
if (proxyAuthToken) {
headers['Authorization'] = `Bearer ${proxyAuthToken}`;
}
fetch(`${getMCPProxyAddress(config)}/config`, { headers })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
setEnv(data.defaultEnvironment); setEnv(data.defaultEnvironment);
@ -359,8 +366,7 @@ const App = () => {
.catch((error) => .catch((error) =>
console.error("Error fetching default environment:", error), console.error("Error fetching default environment:", error),
); );
// eslint-disable-next-line react-hooks/exhaustive-deps }, [config]);
}, []);
useEffect(() => { useEffect(() => {
rootsRef.current = roots; rootsRef.current = roots;

View file

@ -19,7 +19,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import express from "express"; import express from "express";
import { findActualExecutable } from "spawn-rx"; import { findActualExecutable } from "spawn-rx";
import mcpProxy from "./mcpProxy.js"; import mcpProxy from "./mcpProxy.js";
import { randomUUID, randomBytes } from "node:crypto"; import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
const SSE_HEADERS_PASSTHROUGH = ["authorization"]; const SSE_HEADERS_PASSTHROUGH = ["authorization"];
const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [ const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
@ -120,14 +120,38 @@ const authMiddleware = (req: express.Request, res: express.Response, next: expre
return next(); return next();
} }
const authHeader = req.headers.authorization; const sendUnauthorized = () => {
if (!authHeader || authHeader !== `Bearer ${sessionToken}`) {
res.status(401).json({ res.status(401).json({
error: "Unauthorized", error: "Unauthorized",
message: "Authentication required. Use the session token shown in the console when starting the server." message: "Authentication required. Use the session token shown in the console when starting the server."
}); });
};
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
sendUnauthorized();
return; return;
} }
const providedToken = authHeader.substring(7); // Remove 'Bearer ' prefix
const expectedToken = sessionToken;
// Convert to buffers for timing-safe comparison
const providedBuffer = Buffer.from(providedToken);
const expectedBuffer = Buffer.from(expectedToken);
// Check length first to prevent timing attacks
if (providedBuffer.length !== expectedBuffer.length) {
sendUnauthorized();
return;
}
// Perform timing-safe comparison
if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
sendUnauthorized();
return;
}
next(); next();
}; };
@ -444,7 +468,7 @@ app.get("/health", (req, res) => {
}); });
}); });
app.get("/config", (req, res) => { app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
try { try {
res.json({ res.json({
defaultEnvironment, defaultEnvironment,