High severity8.7NVD Advisory· Published Mar 25, 2026· Updated May 10, 2026
CVE-2026-30587
CVE-2026-30587
Description
Multiple Stored XSS vulnerabilities exist in Seafile Server version 13.0.15,13.0.16-pro,12.0.14 and prior and fixed in 13.0.17, 13.0.17-pro, and 12.0.20-pro, via the Seadoc (sdoc) editor. The application fails to properly sanitize WebSocket messages regarding document structure updates. This allows authenticated remote attackers to inject malicious JavaScript payloads via the src attribute of embedded Excalidraw whiteboards or the href attribute of anchor tags
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@seafile/sdoc-editornpm | >= 3.0.0, < 3.0.75 | 3.0.75 |
@seafile/sdoc-editornpm | < 2.0.209 | 2.0.209 |
Affected products
3cpe:2.3:a:seafile:seafile_server:13.0.15:*:*:*:community:*:*:*+ 2 more
- cpe:2.3:a:seafile:seafile_server:13.0.15:*:*:*:community:*:*:*
- cpe:2.3:a:seafile:seafile_server:13.0.16:*:*:*:professional:*:*:*
- cpe:2.3:a:seafile:seafile_server:*:*:*:*:professional:*:*:*range: <12.0.20
Patches
22 files changed · +15 −15
frontend/package.json+2 −2 modified@@ -10,12 +10,12 @@ "@emoji-mart/react": "^1.1.1", "@excalidraw/excalidraw": "^0.18.0", "@gatsbyjs/reach-router": "2.0.1", - "@seafile/comment-editor": "1.0.15", + "@seafile/comment-editor": "1.0.16", "@seafile/react-image-lightbox": "^5.0.4", "@seafile/resumablejs": "1.1.16", "@seafile/seafile-calendar": "1.0.3", "@seafile/seafile-editor": "3.0.16", - "@seafile/seafile-sdoc-editor": "3.0.77", + "@seafile/seafile-sdoc-editor": "3.0.78", "@seafile/stldraw-editor": "1.0.1", "@uiw/codemirror-extensions-langs": "^4.19.4", "@uiw/codemirror-themes": "^4.23.5",
frontend/package-lock.json+13 −13 modified@@ -15,12 +15,12 @@ "@emoji-mart/react": "^1.1.1", "@excalidraw/excalidraw": "^0.18.0", "@gatsbyjs/reach-router": "2.0.1", - "@seafile/comment-editor": "1.0.15", + "@seafile/comment-editor": "1.0.16", "@seafile/react-image-lightbox": "^5.0.4", "@seafile/resumablejs": "1.1.16", "@seafile/seafile-calendar": "1.0.3", "@seafile/seafile-editor": "3.0.16", - "@seafile/seafile-sdoc-editor": "3.0.77", + "@seafile/seafile-sdoc-editor": "3.0.78", "@seafile/stldraw-editor": "1.0.1", "@uiw/codemirror-extensions-langs": "^4.19.4", "@uiw/codemirror-themes": "^4.23.5", @@ -5418,9 +5418,9 @@ "license": "MIT" }, "node_modules/@seafile/comment-editor": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@seafile/comment-editor/-/comment-editor-1.0.15.tgz", - "integrity": "sha512-LPR2XozyHb1X+cMIyhmya8EWvd0AxEg3QBsaC5gpJA9EyfZ+EMlqkAJBye3dRpQEUpIHa9ibk9VIiyub/kTCcQ==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@seafile/comment-editor/-/comment-editor-1.0.16.tgz", + "integrity": "sha512-5VS4TMQdt7c6P12opT2fG5JVF5Mwq1wlNIOP5ptTwH4vIaNqvJac6hUhsnYzXDJdnr4LL0Px1RAAm1aQeWJajg==", "license": "ISC", "dependencies": { "@seafile/react-image-lightbox": "^5.0.4", @@ -5545,12 +5545,12 @@ "license": "MIT" }, "node_modules/@seafile/sdoc-editor": { - "version": "3.0.77", - "resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-3.0.77.tgz", - "integrity": "sha512-S9pL8S5Dka6xntPeyrVew6ZPA0DAir75LMIY2E3Fs3mJJVHZpp2qcmVASjRdtPCJyVkwIp16JjVhWxOfSlf0Ig==", + "version": "3.0.78", + "resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-3.0.78.tgz", + "integrity": "sha512-oReZhj8Ks0Mi+IC4Rw1YOAbsO4klNvzueCcSHNT3fEWqwc+z9f83wXZEWy+0Ti7/7Ek6d3c0pdqPmCRiY2e2ug==", "license": "ISC", "dependencies": { - "@seafile/comment-editor": "~1.0.15", + "@seafile/comment-editor": "~1.0.16", "@seafile/print-js": "1.6.6", "@seafile/react-image-lightbox": "5.0.4", "@seafile/seafile-database": "0.0.19", @@ -5933,13 +5933,13 @@ } }, "node_modules/@seafile/seafile-sdoc-editor": { - "version": "3.0.77", - "resolved": "https://registry.npmjs.org/@seafile/seafile-sdoc-editor/-/seafile-sdoc-editor-3.0.77.tgz", - "integrity": "sha512-69Yq6Yb52rSMos3hPT0e8ImSmR1Y11VL8EFkPgFdMaBPhiVjVVGNrfZj6WWqtXyzrqt71rOx9mxKRxQjcKsJLw==", + "version": "3.0.78", + "resolved": "https://registry.npmjs.org/@seafile/seafile-sdoc-editor/-/seafile-sdoc-editor-3.0.78.tgz", + "integrity": "sha512-+CoV2wWnuCp12xJT0dQEYD0JyVpTzt07iyaKR4xU0ojDB5x/QVTWbT5P44/qhDpo9hLZKfsT74moU3Aw15bEBA==", "license": "ISC", "dependencies": { "@seafile/print-js": "1.6.6", - "@seafile/sdoc-editor": "^3.0.77", + "@seafile/sdoc-editor": "^3.0.78", "classnames": "2.3.2", "dayjs": "1.10.7" },
8fa988aaede0fix: merge 2.0 to fix link&whiteboard xss bug
10 files changed · +90 −33
packages/example/public/locales/en/sdoc-editor.json+2 −1 modified@@ -665,5 +665,6 @@ "Zoom_out": "Zoom out", "Search_page": "Search page", "The_link_address_or_link_page_or_link_block_is_required": "The link address or link page or link block is required", - "Link_page": "Link page" + "Link_page": "Link page", + "Whiteboard_link_invalid_tip": "The whiteboard link is invalid, it needs to be deleted and added again." }
packages/example/public/locales/zh_CN/sdoc-editor.json+3 −2 modified@@ -664,6 +664,7 @@ "Zoom_in": "放大", "Zoom_out": "缩小", "Search_page": "搜索页面", - "The_link_address_or_link_page_or_link_block_is_required": "链接地址或链接页面或链接节点是必填项。", - "Link_page": "链接页面" + "The_link_address_or_link_page_or_link_block_is_required": "链接地址或链接页面或链接节点是必填项。", + "Link_page": "链接页面", + "Whiteboard_link_invalid_tip": "白板链接无效,需要将其删除并重新添加。" }
packages/sdoc-editor/CHANGELOG.md+4 −0 modified@@ -474,6 +474,10 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - add video link in wki ([58ec301](https://github.com/seafileltd/sea-sdoc-editor/commit/58ec301b68083d510ed735500f65f0126a90e074)) - insert video below blocks ([b729201](https://github.com/seafileltd/sea-sdoc-editor/commit/b729201fd9da6e99f94992f71f6571d1694023be)) +## [2.0.209](https://github.com/seafileltd/sea-sdoc-editor/compare/@seafile/sdoc-editor@2.0.208...@seafile/sdoc-editor@2.0.209) (2026-01-31) + +**Note:** Version bump only for package @seafile/sdoc-editor + ## [2.0.208](https://github.com/seafileltd/sea-sdoc-editor/compare/@seafile/sdoc-editor@2.0.207...@seafile/sdoc-editor@2.0.208) (2026-01-06) **Note:** Version bump only for package @seafile/sdoc-editor
packages/sdoc-editor/src/extension/plugins/link/hover/index.js+6 −0 modified@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { Editor } from '@seafile/slate'; import { ReactEditor, useReadOnly } from '@seafile/slate-react'; +import isUrl from 'is-url'; import PropTypes from 'prop-types'; import Tooltip from '../../../../components/tooltip'; import EventBus from '../../../../utils/event-bus'; @@ -21,6 +22,11 @@ const LinkHover = ({ editor, element, menuPosition, onDeleteLink, onEditLink }) }, []); const onMouseDown = useCallback((event) => { + if (!isUrl(element.href)) { + event.preventDefault(); + return; + } + event.stopPropagation(); if (!isWeChat()) { window.open(element.href);
packages/sdoc-editor/src/extension/plugins/link/render-elem.js+9 −0 modified@@ -1,6 +1,7 @@ import React from 'react'; import { Range } from '@seafile/slate'; import classnames from 'classnames'; +import isUrl from 'is-url'; import PropTypes from 'prop-types'; import { INTERNAL_EVENT } from '../../../constants'; import { ScrollContext } from '../../../hooks/use-scroll-context'; @@ -125,6 +126,14 @@ class Link extends React.Component { const { isShowLinkMenu, menuPosition } = this.state; const className = isShowLinkMenu ? 'seafile-ed-hovermenu-mouseclick' : null; + if (!isUrl(element.href)) { + return ( + <span {...attributes}> + <span>{children}</span> + </span> + ); + } + if (readonly) { return ( <span className={classnames(className, 'virtual-link')} {...attributes}>
packages/sdoc-editor/src/extension/plugins/video/render-elem.js+1 −0 modified@@ -172,6 +172,7 @@ const Video = ({ element, editor }) => { observerRefValue = scrollRef.current; resizeObserver = new ResizeObserver((entries) => { + // eslint-disable-next-line no-unused-vars for (let entry of entries) { if (resizeObserver) { onScroll();
packages/sdoc-editor/src/extension/plugins/whiteboard/hover-menu/index.js+24 −20 modified@@ -13,7 +13,7 @@ const propTypes = { onDeleteWhiteboard: PropTypes.func.isRequired, }; -const WhiteboardHoverMenu = ({ menuPosition, onOpen, openFullscreen, onDeleteWhiteboard }) => { +const WhiteboardHoverMenu = ({ isValidUrl, menuPosition, onOpen, openFullscreen, onDeleteWhiteboard }) => { const { t } = useTranslation('sdoc-editor'); const [showTooltip, setShowTooltip] = useState(false); @@ -25,16 +25,18 @@ const WhiteboardHoverMenu = ({ menuPosition, onOpen, openFullscreen, onDeleteWhi <ElementPopover> <div className="sdoc-whiteboard-hover-menu-container" style={menuPosition}> <div className='hover-menu-container'> - <span className='op-group-item'> - <span - id='sdoc_whiteboard_open' - role="button" - className='op-item' - onClick={onOpen} - > - <span>{t('Open')}</span> + {isValidUrl && ( + <span className='op-group-item'> + <span + id='sdoc_whiteboard_open' + role="button" + className='op-item' + onClick={onOpen} + > + <span className='mr-1'>{t('Open')}</span> + </span> </span> - </span> + )} <span className='op-group-item'> <span id='sdoc_whiteboard_delete' @@ -49,20 +51,22 @@ const WhiteboardHoverMenu = ({ menuPosition, onOpen, openFullscreen, onDeleteWhi </Tooltip>} </span> </span> - <span className='op-group-item'> - <span - id='sdoc_whiteboard_full_screen_mode' - role="button" - className='op-item' - onClick={openFullscreen} - > - <i className='sdocfont sdoc-fullscreen'/> - {showTooltip && + {isValidUrl && ( + <span className='op-group-item'> + <span + id='sdoc_whiteboard_full_screen_mode' + role="button" + className='op-item' + onClick={openFullscreen} + > + <i className='sdocfont sdoc-fullscreen icon-font'/> + {showTooltip && <Tooltip target='sdoc_whiteboard_full_screen_mode' placement='top' fade={true}> {t('Full_screen_mode')} </Tooltip>} + </span> </span> - </span> + )} </div> </div> </ElementPopover>
packages/sdoc-editor/src/extension/plugins/whiteboard/index.css+9 −0 modified@@ -58,3 +58,12 @@ height: 80%; overflow: auto; } + +.sdoc-whiteboard-tip { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: red; +}
packages/sdoc-editor/src/extension/plugins/whiteboard/render-elem.js+28 −10 modified@@ -1,8 +1,10 @@ -import React, { useRef, useEffect, useState, useCallback } from 'react'; +import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; import ReactDOM from 'react-dom'; +import { useTranslation } from 'react-i18next'; import { Transforms } from '@seafile/slate'; import { ReactEditor, useReadOnly, useSelected } from '@seafile/slate-react'; import classNames from 'classnames'; +import isUrl from 'is-url'; import { INTERNAL_EVENT } from '../../../constants'; import context from '../../../context'; import { useScrollContext } from '../../../hooks/use-scroll-context'; @@ -20,10 +22,16 @@ const Whiteboard = ({ editor, element }) => { const scrollRef = useScrollContext(); const isSelected = useSelected(); const readOnly = useReadOnly(); + const { t } = useTranslation('sdoc-editor'); const [menuPosition, setMenuPosition] = useState({ top: '', left: '' }); const [isShowZoomOut, setIsShowZoomOut] = useState(false); + const isValidUrl = useMemo(() => { + return isUrl(link); + }, [link]); + useEffect(() => { + if (!isValidUrl) return; const handleMessage = (event) => { if (event.data?.type === 'checkSdocParent') { const isSdocClass = whiteboardRef.current?.classList.contains('sdoc-whiteboard-element'); @@ -64,7 +72,7 @@ const Whiteboard = ({ editor, element }) => { window.removeEventListener('message', handleMessage); unsubscribeResizeArticle(); }; - }, []); + }, [isValidUrl]); const onDeleteWhiteboard = useCallback(() => { const path = ReactEditor.findPath(editor, element); @@ -75,6 +83,7 @@ const Whiteboard = ({ editor, element }) => { const handleDoubleClick = (event) => { event.preventDefault(); + if (!isValidUrl) return; const siteRoot = context.getSetting('siteRoot'); const url = `${siteRoot}lib/${repo_id}/file${file_path}`; window.open(url, '_blank'); @@ -97,6 +106,7 @@ const Whiteboard = ({ editor, element }) => { observerRefValue = scrollRef.current; resizeObserver = new ResizeObserver((entries) => { + // eslint-disable-next-line no-unused-vars for (let entry of entries) { if (resizeObserver) { handleScroll(); @@ -146,17 +156,25 @@ const Whiteboard = ({ editor, element }) => { <> <div className={classNames('sdoc-whiteboard-container', { 'isSelected': isSelected })} onDoubleClick={handleDoubleClick} scrolling='no' > <div className='sdoc-whiteboard-title'>{title}</div> - <iframe - className='sdoc-whiteboard-element' - title={title} - src={link} - ref={whiteboardRef} - > - </iframe> - <div className='iframe-overlay' onDoubleClick={handleDoubleClick} onClick={handleOnClick}></div> + {isValidUrl && ( + <> + <iframe + className='sdoc-whiteboard-element' + title={title} + src={link} + ref={whiteboardRef} + > + </iframe> + <div className='iframe-overlay' onDoubleClick={handleDoubleClick} onClick={handleOnClick}></div> + </> + )} + {!isValidUrl && ( + <div ref={whiteboardRef} className='sdoc-whiteboard-tip'>{t('Whiteboard_link_invalid_tip')}</div> + )} </div> {isSelected && !readOnly && !isShowZoomOut && <WhiteboardHoverMenu + isValidUrl={isValidUrl} menuPosition={menuPosition} onOpen={handleDoubleClick} openFullscreen={openFullscreen}
packages/seafile-sdoc-editor/CHANGELOG.md+4 −0 modified@@ -309,6 +309,10 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline # [3.0.0](https://github.com/seafileltd/sea-sdoc-editor/compare/@seafile/seafile-sdoc-editor@2.0.118...@seafile/seafile-sdoc-editor@3.0.0) (2025-11-03) +## [2.0.126](https://github.com/seafileltd/sea-sdoc-editor/compare/@seafile/seafile-sdoc-editor@2.0.125...@seafile/seafile-sdoc-editor@2.0.126) (2026-01-31) + +**Note:** Version bump only for package @seafile/seafile-sdoc-editor + ## [2.0.125](https://github.com/seafileltd/sea-sdoc-editor/compare/@seafile/seafile-sdoc-editor@2.0.124...@seafile/seafile-sdoc-editor@2.0.125) (2026-01-06) **Note:** Version bump only for package @seafile/seafile-sdoc-editor
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/haiwen/seadoc-editor/commit/8fa988aaede072b2ae073d1b2edcb2fc691423b2nvdPatchWEB
- github.com/haiwen/seahub/commit/4c5301747bdb84c64b2f2b3230417df2d1cc8987nvdPatchWEB
- gist.github.com/gabdevele/1b7e30ab367b26042fa32f45aa12ce2fnvdExploitThird Party AdvisoryWEB
- github.com/advisories/GHSA-rqj3-x344-qvxcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-30587ghsaADVISORY
- manual.seafile.com/12.0/changelog/changelog-for-seafile-professional-serverghsaWEB
- manual.seafile.com/12.0/changelog/changelog-for-seafile-professional-server/nvdRelease Notes
- manual.seafile.com/13.0/changelog/changelog-for-seafile-professional-serverghsaWEB
- manual.seafile.com/13.0/changelog/changelog-for-seafile-professional-server/nvdRelease Notes
- manual.seafile.com/13.0/changelog/server-changelogghsaWEB
- manual.seafile.com/13.0/changelog/server-changelog/nvdRelease Notes
News mentions
0No linked articles in our index yet.