mirror of https://github.com/halo-dev/halo
pref: editor iframe risk with src tag (#6150)
#### What type of PR is this? /kind improvement /area editor /milestone 2.17.x #### What this PR does / why we need it: 在用户设置 iframe 相关的 src 时,检测设置的链接是否符合白名单。如果不符合则不允许设置。 see https://github.com/ueberdosis/tiptap/pull/5160 #### How to test it? 测试在 iframe 中的 src 输入 `javascript: alert("1")` 时是否会触发 javascript #### Does this PR introduce a user-facing change? ```release-note 处理默认编辑器中 iframe 标签的 src 属性可能存在的风险 ```pull/6152/head^2
parent
ed6a5e3898
commit
5aacd8a252
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { i18n } from "@/locales";
|
||||
import type { Editor } from "@/tiptap/vue-3";
|
||||
import { isAllowedUri } from "@/utils/is-allowed-uri";
|
||||
import { computed, type Component } from "vue";
|
||||
import Iframe from "./index";
|
||||
|
||||
|
@ -18,6 +19,9 @@ const src = computed({
|
|||
return props.editor.getAttributes(Iframe.name).src;
|
||||
},
|
||||
set: (src: string) => {
|
||||
if (!src || !isAllowedUri(src)) {
|
||||
return;
|
||||
}
|
||||
props.editor.chain().updateAttributes(Iframe.name, { src: src }).run();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Node as ProseMirrorNode, Decoration } from "@/tiptap/pm";
|
||||
import { i18n } from "@/locales";
|
||||
import type { Decoration, Node as ProseMirrorNode } from "@/tiptap/pm";
|
||||
import type { Editor, Node } from "@/tiptap/vue-3";
|
||||
import { NodeViewWrapper } from "@/tiptap/vue-3";
|
||||
import { isAllowedUri } from "@/utils/is-allowed-uri";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { i18n } from "@/locales";
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor;
|
||||
|
@ -21,6 +22,9 @@ const src = computed({
|
|||
return props.node?.attrs.src;
|
||||
},
|
||||
set: (src: string) => {
|
||||
if (!src || !isAllowedUri(src)) {
|
||||
return;
|
||||
}
|
||||
props.updateAttributes({ src: src });
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,37 +1,38 @@
|
|||
import type { ExtensionOptions, NodeBubbleMenu } from "@/types";
|
||||
import { BlockActionSeparator } from "@/components";
|
||||
import MdiDeleteForeverOutline from "@/components/icon/MdiDeleteForeverOutline.vue";
|
||||
import ToolboxItem from "@/components/toolbox/ToolboxItem.vue";
|
||||
import { i18n } from "@/locales";
|
||||
import type { EditorState } from "@/tiptap/pm";
|
||||
import {
|
||||
Editor,
|
||||
Node,
|
||||
VueNodeViewRenderer,
|
||||
isActive,
|
||||
mergeAttributes,
|
||||
Node,
|
||||
nodeInputRule,
|
||||
nodePasteRule,
|
||||
type Range,
|
||||
VueNodeViewRenderer,
|
||||
} from "@/tiptap/vue-3";
|
||||
import type { EditorState } from "@/tiptap/pm";
|
||||
import type { ExtensionOptions, NodeBubbleMenu } from "@/types";
|
||||
import { deleteNode } from "@/utils";
|
||||
import { isAllowedUri } from "@/utils/is-allowed-uri";
|
||||
import { markRaw } from "vue";
|
||||
import IframeView from "./IframeView.vue";
|
||||
import MdiWeb from "~icons/mdi/web";
|
||||
import ToolboxItem from "@/components/toolbox/ToolboxItem.vue";
|
||||
import { i18n } from "@/locales";
|
||||
import { BlockActionSeparator } from "@/components";
|
||||
import BubbleIframeSize from "./BubbleItemIframeSize.vue";
|
||||
import BubbleIframeLink from "./BubbleItemIframeLink.vue";
|
||||
import MdiBorderAllVariant from "~icons/mdi/border-all-variant";
|
||||
import MdiBorderNoneVariant from "~icons/mdi/border-none-variant";
|
||||
import MdiDesktopMac from "~icons/mdi/desktop-mac";
|
||||
import MdiTabletIpad from "~icons/mdi/tablet-ipad";
|
||||
import MdiCellphoneIphone from "~icons/mdi/cellphone-iphone";
|
||||
import MdiFormatAlignLeft from "~icons/mdi/format-align-left";
|
||||
import MdiDesktopMac from "~icons/mdi/desktop-mac";
|
||||
import MdiFormatAlignCenter from "~icons/mdi/format-align-center";
|
||||
import MdiFormatAlignRight from "~icons/mdi/format-align-right";
|
||||
import MdiFormatAlignJustify from "~icons/mdi/format-align-justify";
|
||||
import { deleteNode } from "@/utils";
|
||||
import MdiDeleteForeverOutline from "@/components/icon/MdiDeleteForeverOutline.vue";
|
||||
import MdiShare from "~icons/mdi/share";
|
||||
import MdiFormatAlignLeft from "~icons/mdi/format-align-left";
|
||||
import MdiFormatAlignRight from "~icons/mdi/format-align-right";
|
||||
import MdiLinkVariant from "~icons/mdi/link-variant";
|
||||
import MdiShare from "~icons/mdi/share";
|
||||
import MdiTabletIpad from "~icons/mdi/tablet-ipad";
|
||||
import MdiWeb from "~icons/mdi/web";
|
||||
import MdiWebSync from "~icons/mdi/web-sync";
|
||||
import BubbleIframeLink from "./BubbleItemIframeLink.vue";
|
||||
import BubbleIframeSize from "./BubbleItemIframeSize.vue";
|
||||
import IframeView from "./IframeView.vue";
|
||||
|
||||
declare module "@/tiptap" {
|
||||
interface Commands<ReturnType> {
|
||||
|
@ -144,11 +145,24 @@ const Iframe = Node.create<ExtensionOptions>({
|
|||
return [
|
||||
{
|
||||
tag: "iframe",
|
||||
getAttrs: (dom) => {
|
||||
const src = (dom as HTMLElement).getAttribute("src");
|
||||
|
||||
// prevent XSS attacks
|
||||
if (!src || !isAllowedUri(src)) {
|
||||
return false;
|
||||
}
|
||||
return { src };
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
// prevent XSS attacks
|
||||
if (!isAllowedUri(HTMLAttributes.src)) {
|
||||
return ["iframe", mergeAttributes({ ...HTMLAttributes, src: "" })];
|
||||
}
|
||||
return ["iframe", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
// From DOMPurify
|
||||
// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js
|
||||
// see https://github.com/ueberdosis/tiptap/pull/5160
|
||||
|
||||
const ATTR_WHITESPACE =
|
||||
/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
|
||||
const IS_ALLOWED_URI =
|
||||
/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape
|
||||
|
||||
export function isAllowedUri(uri: string | undefined) {
|
||||
return !uri || uri.replace(ATTR_WHITESPACE, "").match(IS_ALLOWED_URI);
|
||||
}
|
Loading…
Reference in New Issue