From bb08a6d03bf08bf64a2eb3f46fd9d2fb6184ed05 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sat, 22 Aug 2026 12:38:25 +0200 Subject: [PATCH] fix(mcp): validate OAuth base origin --- .../advanced/environment-variables/index.mdx | 2 +- apps/nextjs/src/env.spec.ts | 22 +++++++++++++++++++ apps/nextjs/src/env.ts | 7 +++++- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 apps/nextjs/src/env.spec.ts diff --git a/apps/docs/docs/advanced/environment-variables/index.mdx b/apps/docs/docs/advanced/environment-variables/index.mdx index 881b1aacc..6837c1abe 100644 --- a/apps/docs/docs/advanced/environment-variables/index.mdx +++ b/apps/docs/docs/advanced/environment-variables/index.mdx @@ -30,7 +30,7 @@ Using the `PUID` and `PGID` will require you to set the correct permissions on t | `PUID` | User ID to run the container as | Any valid user ID | `0` | | `PGID` | Group ID to run the container as | Any valid group ID | `0` | | `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` | -| `BASE_URL` | Public URL used for OAuth metadata and redirects. Set this when Homarr is behind a reverse proxy that does not preserve the public host and protocol. | Any valid absolute URL | - | +| `BASE_URL` | Public HTTP(S) origin used for OAuth metadata and redirects. Set this when Homarr is behind a reverse proxy that does not preserve the public host and protocol. Paths, credentials, query parameters, and fragments are not supported. | For example `https://homarr.example.com` | - | | `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, and RSS feeds | `true` or `false` | `false` | | `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` | | `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` | diff --git a/apps/nextjs/src/env.spec.ts b/apps/nextjs/src/env.spec.ts new file mode 100644 index 000000000..a84e46070 --- /dev/null +++ b/apps/nextjs/src/env.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "vitest"; + +import { baseUrlSchema } from "./env"; + +describe("BASE_URL", () => { + test.each(["http://localhost:7575", "https://homarr.example.com", "https://homarr.example.com/"])( + "accepts the HTTP(S) origin %s", + (value) => { + expect(baseUrlSchema.safeParse(value).success).toBe(true); + }, + ); + + test.each([ + "ftp://homarr.example.com", + "https://user:password@homarr.example.com", + "https://homarr.example.com/prefix", + "https://homarr.example.com?preview=1", + "https://homarr.example.com#preview", + ])("rejects the non-origin URL %s", (value) => { + expect(baseUrlSchema.safeParse(value).success).toBe(false); + }); +}); diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts index c47f4b66f..4d97084ca 100644 --- a/apps/nextjs/src/env.ts +++ b/apps/nextjs/src/env.ts @@ -1,9 +1,14 @@ import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env"; import { z } from "zod/v4"; +export const baseUrlSchema = z.url({ protocol: /^https?$/ }).refine((value) => { + const url = new URL(value); + return !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash; +}, "BASE_URL must be an HTTP(S) origin without credentials, a path, query parameters, or a fragment"); + export const env = createEnv({ server: { - BASE_URL: z.string().url().optional(), + BASE_URL: baseUrlSchema.optional(), UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false), DEMO_MODE: createBooleanSchema(false), DEMO_READ_ONLY: createBooleanSchema(true),