allow loopback to localhost on mcp, only for local MCP clients

This commit is contained in:
Amruth Pillai
2026-04-26 10:59:10 +02:00
parent 76e00ae019
commit 623ca5c675
63 changed files with 84 additions and 460 deletions
+10 -22
View File
@@ -2,9 +2,10 @@ import { createFileRoute } from "@tanstack/react-router";
import { auth } from "@/integrations/auth/config";
import { env } from "@/utils/env";
import { isPrivateOrLoopbackHost, parseAllowedHostList, parseUrl } from "@/utils/url-security";
import { isAllowedOAuthRedirectUri, parseAllowedHostList } from "@/utils/url-security";
const oauthDynamicClientRedirectHosts = parseAllowedHostList(env.OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS);
const oauthTrustedOrigins = [new URL(env.APP_URL).origin.toLowerCase()];
const oauthAuthorizeSanitizedParams = [
"prompt",
"redirect_uri",
@@ -17,25 +18,6 @@ const oauthAuthorizeSanitizedParams = [
"resource",
] as const;
function isAllowedDynamicClientRedirectUri(value: string) {
const parsed = parseUrl(value);
if (!parsed) return false;
if (parsed.protocol !== "https:") return false;
if (parsed.username || parsed.password) return false;
if (parsed.hash) return false;
if (isPrivateOrLoopbackHost(parsed.hostname)) return false;
const appOrigin = new URL(env.APP_URL).origin.toLowerCase();
const origin = parsed.origin.toLowerCase();
const hostname = parsed.hostname.toLowerCase();
if (origin === appOrigin) return true;
if (oauthDynamicClientRedirectHosts.has(origin)) return true;
if (oauthDynamicClientRedirectHosts.has(hostname)) return true;
return false;
}
function sanitizeOAuthAuthorizeRequest(request: Request): Request {
if (request.method !== "GET") return request;
@@ -117,8 +99,14 @@ async function validateDynamicClientRegistrationRequest(request: Request): Promi
const redirectUris = Array.isArray(body.redirect_uris) ? body.redirect_uris : [];
for (const redirectUri of redirectUris) {
if (typeof redirectUri !== "string" || !isAllowedDynamicClientRedirectUri(redirectUri)) {
return Response.json({ message: "redirect_uri is not allowed" }, { status: 400 });
if (
typeof redirectUri !== "string" ||
!isAllowedOAuthRedirectUri(redirectUri, oauthTrustedOrigins, oauthDynamicClientRedirectHosts)
) {
return Response.json(
{ error: "invalid_redirect_uri", error_description: "redirect_uri is not allowed" },
{ status: 400 },
);
}
}
}
+10 -17
View File
@@ -1,5 +1,5 @@
import { t } from "@lingui/core/macro";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { zodValidator } from "@tanstack/zod-adapter";
import { useEffect } from "react";
import { z } from "zod";
@@ -7,7 +7,7 @@ import { z } from "zod";
import { LoadingScreen } from "@/components/layout/loading-screen";
import { ResumePreview } from "@/components/resume/preview";
import { useResumeStore } from "@/components/resume/store/resume";
import { getORPCClient } from "@/integrations/orpc/client";
import { resumeService } from "@/integrations/orpc/services/resume";
import { env } from "@/utils/env";
import { verifyPrinterToken } from "@/utils/printer-token";
@@ -15,6 +15,12 @@ const searchSchema = z.object({
token: z.string().catch(""),
});
const getResumeForPrinterFn = createServerFn({ method: "GET" })
.inputValidator(z.object({ id: z.string(), token: z.string() }))
.handler(({ data }) => {
return resumeService.getByIdForPrinter({ id: data.id, printerToken: data.token });
});
function assertValidPrinterToken(token: string, resumeId: string): void {
const tokenResumeId = verifyPrinterToken(token);
if (tokenResumeId === resumeId) return;
@@ -35,25 +41,12 @@ export const Route = createFileRoute("/printer/$resumeId")({
},
loaderDeps: ({ search }) => ({ token: search.token }),
loader: async ({ params, deps }) => {
const client = getORPCClient();
const resume = await client.resume.getByIdForPrinter({ id: params.resumeId, token: deps.token });
const resume = await getResumeForPrinterFn({ data: { id: params.resumeId, token: deps.token } });
return { resume };
},
head: ({ loaderData }) => ({
meta: [
{
title: loaderData
? `${loaderData.resume.data.basics.name} - ${t({
comment: "Browser tab suffix for printable resume pages",
message: "Resume",
})}`
: t({
comment: "Browser tab title before printable resume data finishes loading",
message: "Resume",
}),
},
],
meta: [{ title: loaderData ? `${loaderData.resume.data.basics.name} - Resume` : "Resume" }],
}),
});