Merge commit from fork
* fix: sanitize user-media svg api endpoint using isomorphic dompurify * fix: add iframe sandbox to prevent priviledge escalation
This commit is contained in:
@@ -25,8 +25,9 @@ const nextConfig: NextConfig = {
|
|||||||
typescript: { ignoreBuildErrors: true },
|
typescript: { ignoreBuildErrors: true },
|
||||||
/**
|
/**
|
||||||
* dockerode is required in the external server packages because of https://github.com/homarr-labs/homarr/issues/612
|
* dockerode is required in the external server packages because of https://github.com/homarr-labs/homarr/issues/612
|
||||||
|
* isomorphic-dompurify and jsdom are required, see https://github.com/kkomelin/isomorphic-dompurify/issues/356
|
||||||
*/
|
*/
|
||||||
serverExternalPackages: ["dockerode"],
|
serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom"],
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
||||||
turbopackFileSystemCacheForDev: true,
|
turbopackFileSystemCacheForDev: true,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
|
"isomorphic-dompurify": "^2.32.0",
|
||||||
"jotai": "^2.15.1",
|
"jotai": "^2.15.1",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "16.0.1",
|
"next": "16.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
|
|
||||||
import { db, eq } from "@homarr/db";
|
import { db, eq } from "@homarr/db";
|
||||||
import { medias } from "@homarr/db/schema";
|
import { medias } from "@homarr/db/schema";
|
||||||
@@ -19,11 +20,24 @@ export async function GET(_req: NextRequest, props: { params: Promise<{ id: stri
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let content = new Uint8Array(image.content);
|
||||||
|
|
||||||
|
// Sanitize SVG content to prevent XSS attacks
|
||||||
|
if (image.contentType === "image/svg+xml" || image.contentType === "image/svg") {
|
||||||
|
const svgText = new TextDecoder().decode(content);
|
||||||
|
const sanitized = DOMPurify.sanitize(svgText, {
|
||||||
|
USE_PROFILES: { svg: true, svgFilters: true },
|
||||||
|
});
|
||||||
|
content = new TextEncoder().encode(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set("Content-Type", image.contentType);
|
headers.set("Content-Type", image.contentType);
|
||||||
headers.set("Content-Length", image.content.length.toString());
|
headers.set("Content-Length", content.length.toString());
|
||||||
|
headers.set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox");
|
||||||
|
headers.set("X-Content-Type-Options", "nosniff");
|
||||||
|
|
||||||
return new NextResponse(new Uint8Array(image.content), {
|
return new NextResponse(content, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import classes from "./component.module.css";
|
|||||||
|
|
||||||
export default function IFrameWidget({ options, isEditMode }: WidgetComponentProps<"iframe">) {
|
export default function IFrameWidget({ options, isEditMode }: WidgetComponentProps<"iframe">) {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { embedUrl, ...permissions } = options;
|
const { embedUrl, allowScrolling, ...permissions } = options;
|
||||||
const allowedPermissions = getAllowedPermissions(permissions);
|
const allowedPermissions = getAllowedPermissions(permissions);
|
||||||
|
const sandboxFlags = getSandboxFlags(permissions);
|
||||||
|
|
||||||
if (embedUrl.trim() === "") return <NoUrl />;
|
if (embedUrl.trim() === "") return <NoUrl />;
|
||||||
if (!isSupportedProtocol(embedUrl)) {
|
if (!isSupportedProtocol(embedUrl)) {
|
||||||
@@ -27,7 +28,8 @@ export default function IFrameWidget({ options, isEditMode }: WidgetComponentPro
|
|||||||
src={embedUrl}
|
src={embedUrl}
|
||||||
title="widget iframe"
|
title="widget iframe"
|
||||||
allow={allowedPermissions.join(" ")}
|
allow={allowedPermissions.join(" ")}
|
||||||
scrolling={options.allowScrolling ? "yes" : "no"}
|
scrolling={allowScrolling ? "yes" : "no"}
|
||||||
|
sandbox={sandboxFlags.join(" ")}
|
||||||
>
|
>
|
||||||
<Text>{t("widget.iframe.error.noBrowerSupport")}</Text>
|
<Text>{t("widget.iframe.error.noBrowerSupport")}</Text>
|
||||||
</iframe>
|
</iframe>
|
||||||
@@ -80,6 +82,22 @@ const getAllowedPermissions = (
|
|||||||
.map(([key]) => permissionMapping[key]);
|
.map(([key]) => permissionMapping[key]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSandboxFlags = (
|
||||||
|
permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl" | "allowScrolling">,
|
||||||
|
) => {
|
||||||
|
const baseSandbox = ["allow-scripts", "allow-same-origin", "allow-forms", "allow-popups"];
|
||||||
|
|
||||||
|
if (permissions.allowFullScreen) {
|
||||||
|
baseSandbox.push("allow-presentation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.allowPayment) {
|
||||||
|
baseSandbox.push("allow-popups-to-escape-sandbox");
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseSandbox;
|
||||||
|
};
|
||||||
|
|
||||||
const permissionMapping = {
|
const permissionMapping = {
|
||||||
allowAutoPlay: "autoplay",
|
allowAutoPlay: "autoplay",
|
||||||
allowCamera: "camera",
|
allowCamera: "camera",
|
||||||
|
|||||||
79
pnpm-lock.yaml
generated
79
pnpm-lock.yaml
generated
@@ -286,6 +286,9 @@ importers:
|
|||||||
glob:
|
glob:
|
||||||
specifier: ^11.0.3
|
specifier: ^11.0.3
|
||||||
version: 11.0.3
|
version: 11.0.3
|
||||||
|
isomorphic-dompurify:
|
||||||
|
specifier: ^2.32.0
|
||||||
|
version: 2.32.0(postcss@8.5.6)
|
||||||
jotai:
|
jotai:
|
||||||
specifier: ^2.15.1
|
specifier: ^2.15.1
|
||||||
version: 2.15.1(@babel/core@7.26.0)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)
|
version: 2.15.1(@babel/core@7.26.0)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)
|
||||||
@@ -2467,6 +2470,9 @@ packages:
|
|||||||
'@acemir/cssom@0.9.19':
|
'@acemir/cssom@0.9.19':
|
||||||
resolution: {integrity: sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==}
|
resolution: {integrity: sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==}
|
||||||
|
|
||||||
|
'@acemir/cssom@0.9.23':
|
||||||
|
resolution: {integrity: sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==}
|
||||||
|
|
||||||
'@actions/core@1.11.1':
|
'@actions/core@1.11.1':
|
||||||
resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==}
|
resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==}
|
||||||
|
|
||||||
@@ -5989,6 +5995,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==}
|
resolution: {integrity: sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
cssstyle@5.3.3:
|
||||||
|
resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
@@ -6257,6 +6267,9 @@ packages:
|
|||||||
dompurify@3.2.6:
|
dompurify@3.2.6:
|
||||||
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
|
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
|
||||||
|
|
||||||
|
dompurify@3.3.0:
|
||||||
|
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
|
||||||
|
|
||||||
dot-case@2.1.1:
|
dot-case@2.1.1:
|
||||||
resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==}
|
resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==}
|
||||||
|
|
||||||
@@ -7689,6 +7702,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
isomorphic-dompurify@2.32.0:
|
||||||
|
resolution: {integrity: sha512-4i6G4ICY57wQpiaNd6WcwhHUAqGDAJGWRlfWKLunBchJjtF2HV4eUeJtUupoEddbnnxYUiRhqfd9e4aDYR7ROA==}
|
||||||
|
engines: {node: '>=20.19.5'}
|
||||||
|
|
||||||
isomorphic-fetch@3.0.0:
|
isomorphic-fetch@3.0.0:
|
||||||
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
|
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
|
||||||
|
|
||||||
@@ -7785,6 +7802,15 @@ packages:
|
|||||||
canvas:
|
canvas:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
jsdom@27.2.0:
|
||||||
|
resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
canvas: ^3.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
canvas:
|
||||||
|
optional: true
|
||||||
|
|
||||||
jsep@1.4.0:
|
jsep@1.4.0:
|
||||||
resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==}
|
resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==}
|
||||||
engines: {node: '>= 10.16.0'}
|
engines: {node: '>= 10.16.0'}
|
||||||
@@ -11111,6 +11137,8 @@ snapshots:
|
|||||||
|
|
||||||
'@acemir/cssom@0.9.19': {}
|
'@acemir/cssom@0.9.19': {}
|
||||||
|
|
||||||
|
'@acemir/cssom@0.9.23': {}
|
||||||
|
|
||||||
'@actions/core@1.11.1':
|
'@actions/core@1.11.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@actions/exec': 1.1.1
|
'@actions/exec': 1.1.1
|
||||||
@@ -15072,6 +15100,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- postcss
|
- postcss
|
||||||
|
|
||||||
|
cssstyle@5.3.3(postcss@8.5.6):
|
||||||
|
dependencies:
|
||||||
|
'@asamuzakjp/css-color': 4.0.4
|
||||||
|
'@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6)
|
||||||
|
css-tree: 3.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- postcss
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
d3-array@3.2.4:
|
d3-array@3.2.4:
|
||||||
@@ -15319,6 +15355,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/trusted-types': 2.0.7
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
|
dompurify@3.3.0:
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
dot-case@2.1.1:
|
dot-case@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
no-case: 2.3.2
|
no-case: 2.3.2
|
||||||
@@ -17018,6 +17058,17 @@ snapshots:
|
|||||||
isexe@3.1.1:
|
isexe@3.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
isomorphic-dompurify@2.32.0(postcss@8.5.6):
|
||||||
|
dependencies:
|
||||||
|
dompurify: 3.3.0
|
||||||
|
jsdom: 27.2.0(postcss@8.5.6)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- canvas
|
||||||
|
- postcss
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
isomorphic-fetch@3.0.0:
|
isomorphic-fetch@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
node-fetch: 2.7.0
|
node-fetch: 2.7.0
|
||||||
@@ -17144,6 +17195,34 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
jsdom@27.2.0(postcss@8.5.6):
|
||||||
|
dependencies:
|
||||||
|
'@acemir/cssom': 0.9.23
|
||||||
|
'@asamuzakjp/dom-selector': 6.7.4
|
||||||
|
cssstyle: 5.3.3(postcss@8.5.6)
|
||||||
|
data-urls: 6.0.0
|
||||||
|
decimal.js: 10.6.0
|
||||||
|
html-encoding-sniffer: 4.0.0
|
||||||
|
http-proxy-agent: 7.0.2
|
||||||
|
https-proxy-agent: 7.0.6
|
||||||
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
parse5: 8.0.0
|
||||||
|
saxes: 6.0.0
|
||||||
|
symbol-tree: 3.2.4
|
||||||
|
tough-cookie: 6.0.0
|
||||||
|
w3c-xmlserializer: 5.0.0
|
||||||
|
webidl-conversions: 8.0.0
|
||||||
|
whatwg-encoding: 3.1.1
|
||||||
|
whatwg-mimetype: 4.0.0
|
||||||
|
whatwg-url: 15.1.0
|
||||||
|
ws: 8.18.3
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- postcss
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
jsep@1.4.0: {}
|
jsep@1.4.0: {}
|
||||||
|
|
||||||
jsesc@3.0.2: {}
|
jsesc@3.0.2: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user