mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
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 },
|
||||
/**
|
||||
* 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: {
|
||||
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
||||
turbopackFileSystemCacheForDev: true,
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"flag-icons": "^7.5.0",
|
||||
"glob": "^11.0.3",
|
||||
"isomorphic-dompurify": "^2.32.0",
|
||||
"jotai": "^2.15.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "16.0.1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { medias } from "@homarr/db/schema";
|
||||
@@ -19,11 +20,24 @@ export async function GET(_req: NextRequest, props: { params: Promise<{ id: stri
|
||||
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();
|
||||
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,
|
||||
headers,
|
||||
});
|
||||
|
||||
@@ -11,8 +11,9 @@ import classes from "./component.module.css";
|
||||
|
||||
export default function IFrameWidget({ options, isEditMode }: WidgetComponentProps<"iframe">) {
|
||||
const t = useI18n();
|
||||
const { embedUrl, ...permissions } = options;
|
||||
const { embedUrl, allowScrolling, ...permissions } = options;
|
||||
const allowedPermissions = getAllowedPermissions(permissions);
|
||||
const sandboxFlags = getSandboxFlags(permissions);
|
||||
|
||||
if (embedUrl.trim() === "") return <NoUrl />;
|
||||
if (!isSupportedProtocol(embedUrl)) {
|
||||
@@ -27,7 +28,8 @@ export default function IFrameWidget({ options, isEditMode }: WidgetComponentPro
|
||||
src={embedUrl}
|
||||
title="widget iframe"
|
||||
allow={allowedPermissions.join(" ")}
|
||||
scrolling={options.allowScrolling ? "yes" : "no"}
|
||||
scrolling={allowScrolling ? "yes" : "no"}
|
||||
sandbox={sandboxFlags.join(" ")}
|
||||
>
|
||||
<Text>{t("widget.iframe.error.noBrowerSupport")}</Text>
|
||||
</iframe>
|
||||
@@ -80,6 +82,22 @@ const getAllowedPermissions = (
|
||||
.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 = {
|
||||
allowAutoPlay: "autoplay",
|
||||
allowCamera: "camera",
|
||||
|
||||
79
pnpm-lock.yaml
generated
79
pnpm-lock.yaml
generated
@@ -286,6 +286,9 @@ importers:
|
||||
glob:
|
||||
specifier: ^11.0.3
|
||||
version: 11.0.3
|
||||
isomorphic-dompurify:
|
||||
specifier: ^2.32.0
|
||||
version: 2.32.0(postcss@8.5.6)
|
||||
jotai:
|
||||
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)
|
||||
@@ -2467,6 +2470,9 @@ packages:
|
||||
'@acemir/cssom@0.9.19':
|
||||
resolution: {integrity: sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==}
|
||||
|
||||
'@acemir/cssom@0.9.23':
|
||||
resolution: {integrity: sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==}
|
||||
|
||||
'@actions/core@1.11.1':
|
||||
resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==}
|
||||
|
||||
@@ -5989,6 +5995,10 @@ packages:
|
||||
resolution: {integrity: sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
cssstyle@5.3.3:
|
||||
resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
@@ -6257,6 +6267,9 @@ packages:
|
||||
dompurify@3.2.6:
|
||||
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
|
||||
|
||||
dompurify@3.3.0:
|
||||
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
|
||||
|
||||
dot-case@2.1.1:
|
||||
resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==}
|
||||
|
||||
@@ -7689,6 +7702,10 @@ packages:
|
||||
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
isomorphic-dompurify@2.32.0:
|
||||
resolution: {integrity: sha512-4i6G4ICY57wQpiaNd6WcwhHUAqGDAJGWRlfWKLunBchJjtF2HV4eUeJtUupoEddbnnxYUiRhqfd9e4aDYR7ROA==}
|
||||
engines: {node: '>=20.19.5'}
|
||||
|
||||
isomorphic-fetch@3.0.0:
|
||||
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
|
||||
|
||||
@@ -7785,6 +7802,15 @@ packages:
|
||||
canvas:
|
||||
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:
|
||||
resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==}
|
||||
engines: {node: '>= 10.16.0'}
|
||||
@@ -11111,6 +11137,8 @@ snapshots:
|
||||
|
||||
'@acemir/cssom@0.9.19': {}
|
||||
|
||||
'@acemir/cssom@0.9.23': {}
|
||||
|
||||
'@actions/core@1.11.1':
|
||||
dependencies:
|
||||
'@actions/exec': 1.1.1
|
||||
@@ -15072,6 +15100,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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: {}
|
||||
|
||||
d3-array@3.2.4:
|
||||
@@ -15319,6 +15355,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dompurify@3.3.0:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dot-case@2.1.1:
|
||||
dependencies:
|
||||
no-case: 2.3.2
|
||||
@@ -17018,6 +17058,17 @@ snapshots:
|
||||
isexe@3.1.1:
|
||||
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:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
@@ -17144,6 +17195,34 @@ snapshots:
|
||||
- supports-color
|
||||
- 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: {}
|
||||
|
||||
jsesc@3.0.2: {}
|
||||
|
||||
Reference in New Issue
Block a user