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:
parent
ab062d8f9d
commit
17740daaf5
2 changed files with 37 additions and 7 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue