diff --git a/packages/components/package.json b/packages/components/package.json index c95b324e1eb..45b737afaa7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -118,6 +118,7 @@ "@types/lodash.throttle": "^4.1.9", "atomico": "^1.75.1", "clsx": "^2.0.0", + "dompurify": "^3.2.5", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "nanoid": "^5.0.9", diff --git a/packages/components/src/__internal__/components/icon.tsx b/packages/components/src/__internal__/components/icon.tsx index a5c0145a332..e44142119b9 100644 --- a/packages/components/src/__internal__/components/icon.tsx +++ b/packages/components/src/__internal__/components/icon.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import DOMPurify from 'dompurify' import { h } from 'vue' h @@ -16,7 +17,7 @@ export function Icon({ icon, class: className, onClick }: IconProps) { onPointerdown={onClick} ref={(el) => { if (el && icon) { - ;(el as HTMLElement).innerHTML = icon.trim() + ;(el as HTMLElement).innerHTML = DOMPurify.sanitize(icon.trim()) } }} /> diff --git a/packages/components/src/code-block/view/components/preview-panel.tsx b/packages/components/src/code-block/view/components/preview-panel.tsx index 1cec6a5ad99..678e1e6b39c 100644 --- a/packages/components/src/code-block/view/components/preview-panel.tsx +++ b/packages/components/src/code-block/view/components/preview-panel.tsx @@ -9,6 +9,7 @@ import { } from 'vue' import type { CodeBlockProps } from './code-block' import clsx from 'clsx' +import DOMPurify from 'dompurify' h Fragment @@ -58,10 +59,11 @@ export const PreviewPanel = defineComponent({ const previewContent = preview.value - if (typeof previewContent === 'string') { - previewContainer.innerHTML = previewContent - } else if (previewContent instanceof HTMLElement) { - previewContainer.appendChild(previewContent) + if ( + typeof previewContent === 'string' || + previewContent instanceof Element + ) { + previewContainer.innerHTML = DOMPurify.sanitize(previewContent) } }) diff --git a/packages/components/src/image-block/view/index.ts b/packages/components/src/image-block/view/index.ts index da379576e44..abf7f86edce 100644 --- a/packages/components/src/image-block/view/index.ts +++ b/packages/components/src/image-block/view/index.ts @@ -6,6 +6,7 @@ import { imageBlockConfig } from '../config' import { withMeta } from '../../__internal__/meta' import { createApp, ref, watchEffect } from 'vue' import { MilkdownImageBlock } from './components/image-block' +import DOMPurify from 'dompurify' export const imageBlockView = $view( imageBlockSchema.node, @@ -19,7 +20,13 @@ export const imageBlockView = $view( const setAttr = (attr: string, value: unknown) => { const pos = getPos() if (pos == null) return - view.dispatch(view.state.tr.setNodeAttribute(pos, attr, value)) + view.dispatch( + view.state.tr.setNodeAttribute( + pos, + attr, + attr === 'src' ? DOMPurify.sanitize(value as string) : value + ) + ) } const config = ctx.get(imageBlockConfig.key) const app = createApp(MilkdownImageBlock, { diff --git a/packages/components/src/image-inline/view.ts b/packages/components/src/image-inline/view.ts index dda97c95169..986dcc87c79 100644 --- a/packages/components/src/image-inline/view.ts +++ b/packages/components/src/image-inline/view.ts @@ -2,9 +2,11 @@ import { $view } from '@milkdown/utils' import type { NodeViewConstructor } from '@milkdown/prose/view' import { imageSchema } from '@milkdown/preset-commonmark' import type { Node } from '@milkdown/prose/model' +import { createApp, ref, watchEffect } from 'vue' +import DOMPurify from 'dompurify' + import { withMeta } from '../__internal__/meta' import { inlineImageConfig } from './config' -import { createApp, ref, watchEffect } from 'vue' import { MilkdownImageInline } from './components/image-inline' export const inlineImageView = $view( @@ -19,8 +21,15 @@ export const inlineImageView = $view( const setAttr = (attr: string, value: unknown) => { const pos = getPos() if (pos == null) return - view.dispatch(view.state.tr.setNodeAttribute(pos, attr, value)) + view.dispatch( + view.state.tr.setNodeAttribute( + pos, + attr, + attr === 'src' ? DOMPurify.sanitize(value as string) : value + ) + ) } + const config = ctx.get(inlineImageConfig.key) const app = createApp(MilkdownImageInline, { src, diff --git a/packages/components/src/link-tooltip/edit/edit-view.ts b/packages/components/src/link-tooltip/edit/edit-view.ts index fa940210c1c..99882631534 100644 --- a/packages/components/src/link-tooltip/edit/edit-view.ts +++ b/packages/components/src/link-tooltip/edit/edit-view.ts @@ -14,6 +14,7 @@ import { } from '../slices' import { createApp, ref, type App, type Ref } from 'vue' import { EditLink } from './component' +import DOMPurify from 'dompurify' interface Data { from: number @@ -80,7 +81,8 @@ export class LinkEditTooltip implements PluginView { const view = this.ctx.get(editorViewCtx) const { from, to, mark } = this.#data const type = linkSchema.type(this.ctx) - if (mark && mark.attrs.href === href) { + const link = DOMPurify.sanitize(href) + if (mark && mark.attrs.href === link) { this.#reset() return } @@ -88,7 +90,7 @@ export class LinkEditTooltip implements PluginView { const tr = view.state.tr if (mark) tr.removeMark(from, to, mark) - tr.addMark(from, to, type.create({ href })) + tr.addMark(from, to, type.create({ href: link })) view.dispatch(tr) this.#reset() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec7c411f61e..0b714ea32e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -316,6 +316,9 @@ importers: clsx: specifier: ^2.0.0 version: 2.1.1 + dompurify: + specifier: ^3.2.5 + version: 3.2.5 lodash.debounce: specifier: ^4.0.8 version: 4.0.8 @@ -2658,6 +2661,9 @@ packages: '@types/rollup-plugin-auto-external@2.0.5': resolution: {integrity: sha512-jspE/1q/4MjrC0lilOpF7Ej5z2vvByPq6lb5ERHt9jCRnp0WJRq+SLK9oPfYroy0xhFhDqQoXLeSHcKqATDaoA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -3280,6 +3286,9 @@ packages: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} engines: {node: '>= 4'} + dompurify@3.2.5: + resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -7434,6 +7443,9 @@ snapshots: dependencies: rollup: 4.39.0 + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -8114,6 +8126,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.2.5: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1