mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 09:24:54 +10:00
allow loopback to localhost on mcp, only for local MCP clients
This commit is contained in:
+10
-22
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }],
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user