mirror of
https://github.com/docmost/docmost.git
synced 2026-05-06 22:03:06 +08:00
fix xss in generic iframe embed (#1419)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Generated
+3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user