VYPR
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.

PackageAffected versionsPatched versions
@seafile/sdoc-editornpm
>= 3.0.0, < 3.0.753.0.75
@seafile/sdoc-editornpm
< 2.0.2092.0.209

Affected products

3
  • cpe: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

2
4c5301747bdb

update sdoc editor version

https://github.com/haiwen/seahub小强Feb 2, 2026via ghsa
2 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"
           },
    
8fa988aaede0

fix: merge 2.0 to fix link&whiteboard xss bug

https://github.com/haiwen/seadoc-editorshuntianJan 31, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.