fix(security): restrict link protocols to http and https (#1888)
This commit is contained in:
@@ -1280,6 +1280,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"noUrl": "No iFrame URL provided",
|
"noUrl": "No iFrame URL provided",
|
||||||
|
"unsupportedProtocol": "The URL provided is using an unsupported protocol. Please use one of ({supportedProtocols})",
|
||||||
"noBrowerSupport": "Your Browser does not support iframes. Please update your browser."
|
"noBrowerSupport": "Your Browser does not support iframes. Please update your browser."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ const manageAppSchema = z.object({
|
|||||||
name: z.string().min(1).max(64),
|
name: z.string().min(1).max(64),
|
||||||
description: z.string().max(512).nullable(),
|
description: z.string().max(512).nullable(),
|
||||||
iconUrl: z.string().min(1),
|
iconUrl: z.string().min(1),
|
||||||
href: z.string().nullable(),
|
href: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.regex(/^https?:\/\//) // Only allow http and https for security reasons (javascript: is not allowed)
|
||||||
|
.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const editAppSchema = manageAppSchema.and(z.object({ id: z.string() }));
|
const editAppSchema = manageAppSchema.and(z.object({ id: z.string() }));
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { createSavePermissionsSchema } from "./permissions";
|
|||||||
|
|
||||||
const integrationCreateSchema = z.object({
|
const integrationCreateSchema = z.object({
|
||||||
name: z.string().nonempty().max(127),
|
name: z.string().nonempty().max(127),
|
||||||
url: z.string().url(),
|
url: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.regex(/^https?:\/\//), // Only allow http and https for security reasons (javascript: is not allowed)
|
||||||
kind: zodEnumFromArray(integrationKinds),
|
kind: zodEnumFromArray(integrationKinds),
|
||||||
secrets: z.array(
|
secrets: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { SearchEngineType } from "@homarr/definitions";
|
|||||||
|
|
||||||
const genericSearchEngine = z.object({
|
const genericSearchEngine = z.object({
|
||||||
type: z.literal("generic" satisfies SearchEngineType),
|
type: z.literal("generic" satisfies SearchEngineType),
|
||||||
urlTemplate: z.string().min(1).startsWith("http").includes("%s"),
|
urlTemplate: z.string().min(1).startsWith("http").includes("%s"), // Only allow http and https for security reasons (javascript: is not allowed)
|
||||||
});
|
});
|
||||||
|
|
||||||
const fromIntegrationSearchEngine = z.object({
|
const fromIntegrationSearchEngine = z.object({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Box, Stack, Text, Title } from "@mantine/core";
|
import { Box, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconBrowserOff } from "@tabler/icons-react";
|
import { IconBrowserOff, IconProtocol } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { objectEntries } from "@homarr/common";
|
import { objectEntries } from "@homarr/common";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
@@ -15,6 +15,9 @@ export default function IFrameWidget({ options, isEditMode }: WidgetComponentPro
|
|||||||
const allowedPermissions = getAllowedPermissions(permissions);
|
const allowedPermissions = getAllowedPermissions(permissions);
|
||||||
|
|
||||||
if (embedUrl.trim() === "") return <NoUrl />;
|
if (embedUrl.trim() === "") return <NoUrl />;
|
||||||
|
if (!isSupportedProtocol(embedUrl)) {
|
||||||
|
return <UnsupportedProtocol />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box h="100%" w="100%">
|
<Box h="100%" w="100%">
|
||||||
@@ -31,6 +34,17 @@ export default function IFrameWidget({ options, isEditMode }: WidgetComponentPro
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const supportedProtocols = ["http", "https"];
|
||||||
|
|
||||||
|
const isSupportedProtocol = (url: string) => {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
return supportedProtocols.map((protocol) => `${protocol}:`).includes(`${parsedUrl.protocol}`);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const NoUrl = () => {
|
const NoUrl = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
@@ -42,6 +56,21 @@ const NoUrl = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UnsupportedProtocol = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" h="100%">
|
||||||
|
<IconProtocol />
|
||||||
|
<Title order={4} ta="center">
|
||||||
|
{t("widget.iframe.error.unsupportedProtocol", {
|
||||||
|
supportedProtocols: supportedProtocols.map((protocol) => protocol).join(", "),
|
||||||
|
})}
|
||||||
|
</Title>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getAllowedPermissions = (permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl">) => {
|
const getAllowedPermissions = (permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl">) => {
|
||||||
return objectEntries(permissions)
|
return objectEntries(permissions)
|
||||||
.filter(([_key, value]) => value)
|
.filter(([_key, value]) => value)
|
||||||
|
|||||||
Reference in New Issue
Block a user