mirror of
https://github.com/docmost/docmost.git
synced 2025-11-11 02:02:07 +10:00
fix xss in generic iframe embed (#1419)
This commit is contained in:
@ -21,6 +21,7 @@ import i18n from "i18next";
|
|||||||
import {
|
import {
|
||||||
getEmbedProviderById,
|
getEmbedProviderById,
|
||||||
getEmbedUrlAndProvider,
|
getEmbedUrlAndProvider,
|
||||||
|
sanitizeUrl,
|
||||||
} from "@docmost/editor-ext";
|
} from "@docmost/editor-ext";
|
||||||
import { ResizableWrapper } from "../common/resizable-wrapper";
|
import { ResizableWrapper } from "../common/resizable-wrapper";
|
||||||
import classes from "./embed-view.module.css";
|
import classes from "./embed-view.module.css";
|
||||||
@ -51,9 +52,12 @@ export default function EmbedView(props: NodeViewProps) {
|
|||||||
validate: zodResolver(schema),
|
validate: zodResolver(schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleResize = useCallback((newHeight: number) => {
|
const handleResize = useCallback(
|
||||||
updateAttributes({ height: newHeight });
|
(newHeight: number) => {
|
||||||
}, [updateAttributes]);
|
updateAttributes({ height: newHeight });
|
||||||
|
},
|
||||||
|
[updateAttributes],
|
||||||
|
);
|
||||||
|
|
||||||
async function onSubmit(data: { url: string }) {
|
async function onSubmit(data: { url: string }) {
|
||||||
if (!editor.isEditable) {
|
if (!editor.isEditable) {
|
||||||
@ -63,11 +67,11 @@ export default function EmbedView(props: NodeViewProps) {
|
|||||||
if (provider) {
|
if (provider) {
|
||||||
const embedProvider = getEmbedProviderById(provider);
|
const embedProvider = getEmbedProviderById(provider);
|
||||||
if (embedProvider.id === "iframe") {
|
if (embedProvider.id === "iframe") {
|
||||||
updateAttributes({ src: data.url });
|
updateAttributes({ src: sanitizeUrl(data.url) });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (embedProvider.regex.test(data.url)) {
|
if (embedProvider.regex.test(data.url)) {
|
||||||
updateAttributes({ src: data.url });
|
updateAttributes({ src: sanitizeUrl(data.url) });
|
||||||
} else {
|
} else {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: t("Invalid {{provider}} embed link", {
|
message: t("Invalid {{provider}} embed link", {
|
||||||
@ -95,7 +99,7 @@ export default function EmbedView(props: NodeViewProps) {
|
|||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
className={classes.embedIframe}
|
className={classes.embedIframe}
|
||||||
src={embedUrl}
|
src={sanitizeUrl(embedUrl)}
|
||||||
allow="encrypted-media"
|
allow="encrypted-media"
|
||||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
"dev": "pnpm concurrently -n \"frontend,backend\" -c \"cyan,green\" \"pnpm run client:dev\" \"pnpm run server:dev\""
|
"dev": "pnpm concurrently -n \"frontend,backend\" -c \"cyan,green\" \"pnpm run client:dev\" \"pnpm run server:dev\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@braintree/sanitize-url": "^7.1.0",
|
||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@hocuspocus/extension-redis": "^2.15.2",
|
"@hocuspocus/extension-redis": "^2.15.2",
|
||||||
"@hocuspocus/provider": "^2.15.2",
|
"@hocuspocus/provider": "^2.15.2",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { sanitizeUrl } from './utils';
|
||||||
|
|
||||||
export interface EmbedOptions {
|
export interface EmbedOptions {
|
||||||
HTMLAttributes: Record<string, any>;
|
HTMLAttributes: Record<string, any>;
|
||||||
@ -40,9 +41,12 @@ export const Embed = Node.create<EmbedOptions>({
|
|||||||
return {
|
return {
|
||||||
src: {
|
src: {
|
||||||
default: '',
|
default: '',
|
||||||
parseHTML: (element) => element.getAttribute('data-src'),
|
parseHTML: (element) => {
|
||||||
|
const src = element.getAttribute('data-src');
|
||||||
|
return sanitizeUrl(src);
|
||||||
|
},
|
||||||
renderHTML: (attributes: EmbedAttributes) => ({
|
renderHTML: (attributes: EmbedAttributes) => ({
|
||||||
'data-src': attributes.src,
|
'data-src': sanitizeUrl(attributes.src),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
provider: {
|
provider: {
|
||||||
@ -85,6 +89,9 @@ export const Embed = Node.create<EmbedOptions>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
const src = HTMLAttributes["data-src"];
|
||||||
|
const safeHref = sanitizeUrl(src);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"div",
|
"div",
|
||||||
mergeAttributes(
|
mergeAttributes(
|
||||||
@ -95,10 +102,10 @@ export const Embed = Node.create<EmbedOptions>({
|
|||||||
[
|
[
|
||||||
"a",
|
"a",
|
||||||
{
|
{
|
||||||
href: HTMLAttributes["data-src"],
|
href: safeHref,
|
||||||
target: "blank",
|
target: "blank",
|
||||||
},
|
},
|
||||||
`${HTMLAttributes["data-src"]}`,
|
safeHref,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@ -108,9 +115,15 @@ export const Embed = Node.create<EmbedOptions>({
|
|||||||
setEmbed:
|
setEmbed:
|
||||||
(attrs: EmbedAttributes) =>
|
(attrs: EmbedAttributes) =>
|
||||||
({ commands }) => {
|
({ commands }) => {
|
||||||
|
// Validate the URL before inserting
|
||||||
|
const validatedAttrs = {
|
||||||
|
...attrs,
|
||||||
|
src: sanitizeUrl(attrs.src),
|
||||||
|
};
|
||||||
|
|
||||||
return commands.insertContent({
|
return commands.insertContent({
|
||||||
type: 'embed',
|
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 { CellSelection, TableMap } from "@tiptap/pm/tables";
|
||||||
import { Node, ResolvedPos } from "@tiptap/pm/model";
|
import { Node, ResolvedPos } from "@tiptap/pm/model";
|
||||||
import Table from "@tiptap/extension-table";
|
import Table from "@tiptap/extension-table";
|
||||||
|
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url";
|
||||||
|
|
||||||
export const isRectSelected = (rect: any) => (selection: CellSelection) => {
|
export const isRectSelected = (rect: any) => (selection: CellSelection) => {
|
||||||
const map = TableMap.get(selection.$anchorCell.node(-1));
|
const map = TableMap.get(selection.$anchorCell.node(-1));
|
||||||
@ -379,3 +380,12 @@ export function setAttributes(
|
|||||||
export function icon(name: string) {
|
export function icon(name: string) {
|
||||||
return `<span class="ProseMirror-icon ProseMirror-icon-${name}"></span>`;
|
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
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -16,6 +16,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@braintree/sanitize-url':
|
||||||
|
specifier: ^7.1.0
|
||||||
|
version: 7.1.0
|
||||||
'@docmost/editor-ext':
|
'@docmost/editor-ext':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/editor-ext
|
version: link:packages/editor-ext
|
||||||
|
|||||||
Reference in New Issue
Block a user