Skip to content

Commit

Permalink
fix(security): restrict link protocols to http and https (#1888)
Browse files Browse the repository at this point in the history
  • Loading branch information
Meierschlumpf authored Jan 10, 2025
1 parent 80c02ef commit a12dd10
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 4 deletions.
1 change: 1 addition & 0 deletions packages/translation/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,7 @@
},
"error": {
"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."
}
},
Expand Down
6 changes: 5 additions & 1 deletion packages/validation/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ const manageAppSchema = z.object({
name: z.string().min(1).max(64),
description: z.string().max(512).nullable(),
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() }));
Expand Down
5 changes: 4 additions & 1 deletion packages/validation/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { createSavePermissionsSchema } from "./permissions";

const integrationCreateSchema = z.object({
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),
secrets: z.array(
z.object({
Expand Down
2 changes: 1 addition & 1 deletion packages/validation/src/search-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { SearchEngineType } from "@homarr/definitions";

const genericSearchEngine = z.object({
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({
Expand Down
31 changes: 30 additions & 1 deletion packages/widgets/src/iframe/component.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

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 { useI18n } from "@homarr/translation/client";
Expand All @@ -15,6 +15,9 @@ export default function IFrameWidget({ options, isEditMode }: WidgetComponentPro
const allowedPermissions = getAllowedPermissions(permissions);

if (embedUrl.trim() === "") return <NoUrl />;
if (!isSupportedProtocol(embedUrl)) {
return <UnsupportedProtocol />;
}

return (
<Box h="100%" w="100%">
Expand All @@ -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 t = useI18n();

Expand All @@ -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">) => {
return objectEntries(permissions)
.filter(([_key, value]) => value)
Expand Down

0 comments on commit a12dd10

Please sign in to comment.