Skip to content

Commit 0d7757e

Browse files
cliffhallclaude
andauthored
fix: validate serverInfo.websiteUrl before rendering as link (#1186)
* fix: validate serverInfo.websiteUrl before rendering as link A connected MCP server's serverInfo.websiteUrl was rendered directly as an <a href> with no protocol check, allowing a malicious server to supply javascript: (or other unsafe) URLs that would execute on click. Run the value through validateRedirectUrl and, on failure, show a warning-colored "Potentially malicious websiteURL field" message instead of a link. The offending URL is still surfaced via tooltip for inspection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: render websiteUrl warning below server name Move the "Potentially malicious websiteURL field" message onto its own row beneath the icon+name row so it doesn't crowd the server name horizontally in the narrow sidebar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a809c2a commit 0d7757e

2 files changed

Lines changed: 131 additions & 37 deletions

File tree

client/src/components/Sidebar.tsx

Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Copy,
1616
CheckCheck,
1717
Server,
18+
AlertTriangle,
1819
} from "lucide-react";
1920
import { Button } from "@/components/ui/button";
2021
import { Input } from "@/components/ui/input";
@@ -42,6 +43,7 @@ import CustomHeaders from "./CustomHeaders";
4243
import { CustomHeaders as CustomHeadersType } from "@/lib/types/customHeaders";
4344
import { useToast } from "../lib/hooks/useToast";
4445
import IconDisplay, { WithIcons } from "./IconDisplay";
46+
import { validateRedirectUrl } from "@/utils/urlValidation";
4547

4648
interface SidebarProps {
4749
connectionStatus: ConnectionStatus;
@@ -782,44 +784,72 @@ const Sidebar = ({
782784
</span>
783785
</div>
784786

785-
{connectionStatus === "connected" && serverImplementation && (
786-
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg mb-4">
787-
<div className="flex items-center gap-2 mb-1">
788-
{(serverImplementation as WithIcons).icons &&
789-
(serverImplementation as WithIcons).icons!.length > 0 ? (
790-
<IconDisplay
791-
icons={(serverImplementation as WithIcons).icons}
792-
size="sm"
793-
/>
794-
) : (
795-
<Server className="w-4 h-4 text-gray-500" />
796-
)}
797-
{(serverImplementation as { websiteUrl?: string })
798-
.websiteUrl ? (
799-
<a
800-
href={
801-
(serverImplementation as { websiteUrl?: string })
802-
.websiteUrl
803-
}
804-
target="_blank"
805-
rel="noopener noreferrer"
806-
className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors"
807-
>
808-
{serverImplementation.name || "MCP Server"}
809-
</a>
810-
) : (
811-
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
812-
{serverImplementation.name || "MCP Server"}
813-
</span>
814-
)}
815-
</div>
816-
{serverImplementation.version && (
817-
<div className="text-xs text-gray-500 dark:text-gray-400">
818-
Version: {serverImplementation.version}
787+
{connectionStatus === "connected" &&
788+
serverImplementation &&
789+
(() => {
790+
const websiteUrl = (
791+
serverImplementation as { websiteUrl?: string }
792+
).websiteUrl;
793+
let isValidWebsiteUrl = false;
794+
if (websiteUrl) {
795+
try {
796+
validateRedirectUrl(websiteUrl);
797+
isValidWebsiteUrl = true;
798+
} catch {
799+
isValidWebsiteUrl = false;
800+
}
801+
}
802+
const serverName = serverImplementation.name || "MCP Server";
803+
return (
804+
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg mb-4">
805+
<div className="flex items-center gap-2 mb-1">
806+
{(serverImplementation as WithIcons).icons &&
807+
(serverImplementation as WithIcons).icons!.length > 0 ? (
808+
<IconDisplay
809+
icons={(serverImplementation as WithIcons).icons}
810+
size="sm"
811+
/>
812+
) : (
813+
<Server className="w-4 h-4 text-gray-500" />
814+
)}
815+
{websiteUrl && isValidWebsiteUrl ? (
816+
<a
817+
href={websiteUrl}
818+
target="_blank"
819+
rel="noopener noreferrer"
820+
className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors"
821+
>
822+
{serverName}
823+
</a>
824+
) : (
825+
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
826+
{serverName}
827+
</span>
828+
)}
829+
</div>
830+
{websiteUrl && !isValidWebsiteUrl && (
831+
<Tooltip>
832+
<TooltipTrigger asChild>
833+
<div className="flex items-center gap-1 text-yellow-600 dark:text-yellow-500 mb-1">
834+
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
835+
<span className="text-xs font-normal">
836+
Potentially malicious websiteURL field
837+
</span>
838+
</div>
839+
</TooltipTrigger>
840+
<TooltipContent>
841+
<p className="max-w-xs break-all">{websiteUrl}</p>
842+
</TooltipContent>
843+
</Tooltip>
844+
)}
845+
{serverImplementation.version && (
846+
<div className="text-xs text-gray-500 dark:text-gray-400">
847+
Version: {serverImplementation.version}
848+
</div>
849+
)}
819850
</div>
820-
)}
821-
</div>
822-
)}
851+
);
852+
})()}
823853

824854
{loggingSupported && connectionStatus === "connected" && (
825855
<div className="space-y-2">

client/src/components/__tests__/Sidebar.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,4 +1046,68 @@ describe("Sidebar", () => {
10461046
);
10471047
});
10481048
});
1049+
1050+
describe("Server Implementation websiteUrl", () => {
1051+
it("should render a warning instead of a link when websiteUrl uses an unsafe protocol", () => {
1052+
renderSidebar({
1053+
connectionStatus: "connected",
1054+
serverImplementation: {
1055+
name: "Evil Server",
1056+
version: "1.0.0",
1057+
websiteUrl: "javascript:alert('xss')",
1058+
},
1059+
});
1060+
1061+
// The warning message should be displayed
1062+
expect(
1063+
screen.getByText("Potentially malicious websiteURL field"),
1064+
).toBeInTheDocument();
1065+
1066+
// The server name should still be shown
1067+
expect(screen.getByText("Evil Server")).toBeInTheDocument();
1068+
1069+
// No link should have been rendered for the malicious URL
1070+
expect(
1071+
screen.queryByRole("link", { name: "Evil Server" }),
1072+
).not.toBeInTheDocument();
1073+
});
1074+
1075+
it("should render a link when websiteUrl is a valid https URL", () => {
1076+
renderSidebar({
1077+
connectionStatus: "connected",
1078+
serverImplementation: {
1079+
name: "Good Server",
1080+
version: "1.0.0",
1081+
websiteUrl: "https://example.com",
1082+
},
1083+
});
1084+
1085+
const link = screen.getByRole("link", { name: "Good Server" });
1086+
expect(link).toBeInTheDocument();
1087+
expect(link).toHaveAttribute("href", "https://example.com");
1088+
1089+
expect(
1090+
screen.queryByText("Potentially malicious websiteURL field"),
1091+
).not.toBeInTheDocument();
1092+
});
1093+
1094+
it("should render a link when websiteUrl is a valid http URL", () => {
1095+
renderSidebar({
1096+
connectionStatus: "connected",
1097+
serverImplementation: {
1098+
name: "Local Server",
1099+
version: "1.0.0",
1100+
websiteUrl: "http://localhost:3000",
1101+
},
1102+
});
1103+
1104+
const link = screen.getByRole("link", { name: "Local Server" });
1105+
expect(link).toBeInTheDocument();
1106+
expect(link).toHaveAttribute("href", "http://localhost:3000");
1107+
1108+
expect(
1109+
screen.queryByText("Potentially malicious websiteURL field"),
1110+
).not.toBeInTheDocument();
1111+
});
1112+
});
10491113
});

0 commit comments

Comments
 (0)