From 254438247e9b9b7534ffddbf23d6721ea35ed436 Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 26 Aug 2026 15:07:49 +0200 Subject: [PATCH] fix: allow same-origin iframe paths --- packages/widgets/src/iframe/component.tsx | 6 +++--- packages/widgets/src/iframe/index.ts | 4 ++-- packages/widgets/src/iframe/url.ts | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 packages/widgets/src/iframe/url.ts diff --git a/packages/widgets/src/iframe/component.tsx b/packages/widgets/src/iframe/component.tsx index c66343927..eef89b172 100644 --- a/packages/widgets/src/iframe/component.tsx +++ b/packages/widgets/src/iframe/component.tsx @@ -7,13 +7,13 @@ import { objectEntries } from "@homarr/common"; import { useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; -import { getSafeApplicationUrl } from "../common/application-url"; import classes from "./component.module.css"; +import { getSafeIframeUrl } from "./url"; export default function IFrameWidget({ options, isEditMode }: WidgetComponentProps<"iframe">) { const t = useI18n("widget.iframe"); const { embedUrl, allowScrolling, ...permissions } = options; - const safeEmbedUrl = getSafeApplicationUrl(embedUrl); + const safeEmbedUrl = getSafeIframeUrl(embedUrl); const allowedPermissions = getAllowedPermissions(permissions); const sandboxFlags = getSandboxFlags(permissions); @@ -45,7 +45,7 @@ export default function IFrameWidget({ options, isEditMode }: WidgetComponentPro const supportedProtocols = ["http", "https"]; export const isSupportedProtocol = (url: string) => { - return getSafeApplicationUrl(url) !== undefined; + return getSafeIframeUrl(url) !== undefined; }; const NoUrl = () => { diff --git a/packages/widgets/src/iframe/index.ts b/packages/widgets/src/iframe/index.ts index 981ab180c..512f3278b 100644 --- a/packages/widgets/src/iframe/index.ts +++ b/packages/widgets/src/iframe/index.ts @@ -1,8 +1,8 @@ import { IconBrowser, IconExternalLink } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; -import { getSafeApplicationUrl } from "../common/application-url"; import { optionsBuilder } from "../options"; +import { getSafeIframeUrl } from "./url"; const createOptions = () => optionsBuilder.from((factory) => ({ @@ -23,7 +23,7 @@ export const { definition, componentLoader } = createWidgetDefinition("iframe", supportsAdvancedFocus: false, icon: IconBrowser, contextActions: ({ options }) => { - const embedUrl = getSafeApplicationUrl(options.embedUrl); + const embedUrl = getSafeIframeUrl(options.embedUrl); return embedUrl ? [ { diff --git a/packages/widgets/src/iframe/url.ts b/packages/widgets/src/iframe/url.ts new file mode 100644 index 000000000..a76e5da42 --- /dev/null +++ b/packages/widgets/src/iframe/url.ts @@ -0,0 +1,19 @@ +import { getSafeApplicationUrl } from "../common/application-url"; + +const relativeUrlBase = "https://homarr.invalid"; + +export const getSafeIframeUrl = (value: unknown): string | undefined => { + const absoluteUrl = getSafeApplicationUrl(value); + if (absoluteUrl) return absoluteUrl; + if (typeof value !== "string") return undefined; + + const path = value.trim(); + if (!path.startsWith("/")) return undefined; + + const resolvedUrl = getSafeApplicationUrl(path, { baseUrl: relativeUrlBase }); + if (!resolvedUrl) return undefined; + + const parsedUrl = new URL(resolvedUrl); + if (parsedUrl.origin !== relativeUrlBase) return undefined; + return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; +};