fix xss in generic iframe embed (#1419)

This commit is contained in:
Philip Okugbe
2025-07-29 19:28:48 +01:00
committed by GitHub
parent 78bce0e29d
commit 6b627d289c
5 changed files with 42 additions and 11 deletions
@@ -21,6 +21,7 @@ import i18n from "i18next";
import {
getEmbedProviderById,
getEmbedUrlAndProvider,
sanitizeUrl,
} from "@docmost/editor-ext";
import { ResizableWrapper } from "../common/resizable-wrapper";
import classes from "./embed-view.module.css";
@@ -51,9 +52,12 @@ export default function EmbedView(props: NodeViewProps) {
validate: zodResolver(schema),
});
const handleResize = useCallback((newHeight: number) => {
updateAttributes({ height: newHeight });
}, [updateAttributes]);
const handleResize = useCallback(
(newHeight: number) => {
updateAttributes({ height: newHeight });
},
[updateAttributes],
);
async function onSubmit(data: { url: string }) {
if (!editor.isEditable) {
@@ -63,11 +67,11 @@ export default function EmbedView(props: NodeViewProps) {
if (provider) {
const embedProvider = getEmbedProviderById(provider);
if (embedProvider.id === "iframe") {
updateAttributes({ src: data.url });
updateAttributes({ src: sanitizeUrl(data.url) });
return;
}
if (embedProvider.regex.test(data.url)) {
updateAttributes({ src: data.url });
updateAttributes({ src: sanitizeUrl(data.url) });
} else {
notifications.show({
message: t("Invalid {{provider}} embed link", {
@@ -95,7 +99,7 @@ export default function EmbedView(props: NodeViewProps) {
>
<iframe
className={classes.embedIframe}
src={embedUrl}
src={sanitizeUrl(embedUrl)}
allow="encrypted-media"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
allowFullScreen
+1
View File
@@ -18,6 +18,7 @@
"dev": "pnpm concurrently -n \"frontend,backend\" -c \"cyan,green\" \"pnpm run client:dev\" \"pnpm run server:dev\""
},
"dependencies": {
"@braintree/sanitize-url": "^7.1.0",
"@docmost/editor-ext": "workspace:*",
"@hocuspocus/extension-redis": "^2.15.2",
"@hocuspocus/provider": "^2.15.2",
+18 -5
View File
@@ -1,5 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import { sanitizeUrl } from './utils';
export interface EmbedOptions {
HTMLAttributes: Record<string, any>;
@@ -40,9 +41,12 @@ export const Embed = Node.create<EmbedOptions>({
return {
src: {
default: '',
parseHTML: (element) => element.getAttribute('data-src'),
parseHTML: (element) => {
const src = element.getAttribute('data-src');
return sanitizeUrl(src);
},
renderHTML: (attributes: EmbedAttributes) => ({
'data-src': attributes.src,
'data-src': sanitizeUrl(attributes.src),
}),
},
provider: {
@@ -85,6 +89,9 @@ export const Embed = Node.create<EmbedOptions>({
},
renderHTML({ HTMLAttributes }) {
const src = HTMLAttributes["data-src"];
const safeHref = sanitizeUrl(src);
return [
"div",
mergeAttributes(
@@ -95,10 +102,10 @@ export const Embed = Node.create<EmbedOptions>({
[
"a",
{
href: HTMLAttributes["data-src"],
href: safeHref,
target: "blank",
},
`${HTMLAttributes["data-src"]}`,
safeHref,
],
];
},
@@ -108,9 +115,15 @@ export const Embed = Node.create<EmbedOptions>({
setEmbed:
(attrs: EmbedAttributes) =>
({ commands }) => {
// Validate the URL before inserting
const validatedAttrs = {
...attrs,
src: sanitizeUrl(attrs.src),
};
return commands.insertContent({
type: 'embed',
attrs: attrs,
attrs: validatedAttrs,
});
},
};
+10
View File
@@ -4,6 +4,7 @@ import { Selection, Transaction } from "@tiptap/pm/state";
import { CellSelection, TableMap } from "@tiptap/pm/tables";
import { Node, ResolvedPos } from "@tiptap/pm/model";
import Table from "@tiptap/extension-table";
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url";
export const isRectSelected = (rect: any) => (selection: CellSelection) => {
const map = TableMap.get(selection.$anchorCell.node(-1));
@@ -379,3 +380,12 @@ export function setAttributes(
export function icon(name: string) {
return `<span class="ProseMirror-icon ProseMirror-icon-${name}"></span>`;
}
export function sanitizeUrl(url: string | undefined): string {
if (!url) return "";
const sanitized = braintreeSanitizeUrl(url);
// Return empty string instead of "about:blank"
return sanitized === "about:blank" ? "" : sanitized;
}
+3
View File
@@ -16,6 +16,9 @@ importers:
.:
dependencies:
'@braintree/sanitize-url':
specifier: ^7.1.0
version: 7.1.0
'@docmost/editor-ext':
specifier: workspace:*
version: link:packages/editor-ext