CVE-2022-25037
Description
An issue in wanEditor v4.7.11 and fixed in v.4.7.12 and v.5 was discovered to contain a cross-site scripting (XSS) vulnerability via the image upload function.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
WangEditor v4.7.11 has a stored XSS vulnerability in image upload, allowing attackers to inject arbitrary JavaScript via crafted filenames or URLs.
Root
Cause
WangEditor v4.7.11, an open-source web rich text editor, contains a cross-site scripting (XSS) vulnerability in its image upload functionality. The issue arises because the editor does not properly sanitize user-supplied input when constructing HTML elements for inserted images. Specifically, the code concatenates user-controlled strings (such as image URLs or alt text) directly into template literals, without escaping dangerous characters like <, >, or ". This allows an attacker to break out of the attribute context and inject arbitrary HTML or JavaScript. The fix, implemented in commits for versions 4.7.12 and v5, applies proper escaping using .replace(/</g, '<') and avoids string concatenation by using linkDom.href = link and resultText for inner text [1][4].
Exploitation
An attacker can exploit this vulnerability by crafting a malicious payload as the image filename or URL during the upload process. For example, setting the image source to "> causes the editor to inject an ` element with an onerror` event handler that executes arbitrary JavaScript. The attack does not require authentication if the editor is publicly accessible, and any user who views the content containing the malicious image will trigger the payload. The NVD notes that a proof-of-concept exists demonstrating successful XSS via image, video, and code insertion interfaces [2][4].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim's browser session. This can lead to session hijacking, cookie theft, defacement of the editor's output, or redirection to malicious sites. Since the injected script can access the DOM and make same-origin requests, the impact is elevated when the editor is used in applications handling sensitive data.
Mitigation
Users are strongly advised to upgrade to WangEditor v4.7.12 or v5, which contain the security fix that properly escapes input before rendering [1]. No workarounds have been published, but restricting the ability to upload or modify images to trusted users can reduce risk.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@wangeditor/editornpm | < 4.7.12 | 4.7.12 |
Affected products
2- Range: <= 4.7.11
Patches
18 files changed · +176 −28
src/menus/code/create-panel-conf.ts+14 −15 modified@@ -20,7 +20,7 @@ export default function (editor: Editor, text: string, languageType: string): Pa * 插入代码块 * @param text 文字 */ - function insertCode(text: string): void { + function insertCode(languateType: string, code: string): void { // 选区处于链接中,则选中整个菜单,再执行 insertHTML let active = isActive(editor) @@ -29,10 +29,21 @@ export default function (editor: Editor, text: string, languageType: string): Pa } const content = editor.selection.getSelectionStartElem()?.elems[0].innerHTML + if (content) { editor.cmd.do('insertHTML', EMPTY_P) } - editor.cmd.do('insertHTML', text) + + // 过滤标签,防止xss + let formatCode = code.replace(/</g, '<').replace(/>/g, '>') + + // 高亮渲染 + if (editor.highlight) { + formatCode = editor.highlight.highlightAuto(formatCode).value + } + + //增加pre标签 + editor.cmd.do('insertHTML', `<pre><code class="${languateType}">${formatCode}</code></pre>`) const $code = editor.selection.getSelectionStartElem() const $codeElem = $code?.getNodeTop(editor) @@ -109,34 +120,22 @@ export default function (editor: Editor, text: string, languageType: string): Pa selector: '#' + btnOkId, type: 'click', fn: () => { - let formatCode, codeDom - const $code = document.getElementById(inputIFrameId) const $select = $('#' + languageId) let languageType = $select.val() // @ts-ignore let code = $code.value - // 高亮渲染 - if (editor.highlight) { - formatCode = editor.highlight.highlightAuto(code).value - } else { - formatCode = `<xmp>${code}</xmp>` - } - // 代码为空,则不插入 if (!code) return //增加标签 if (isActive(editor)) { return false } else { - //增加pre标签 - codeDom = `<pre><code class="${languageType}">${formatCode}</code></pre>` - // @ts-ignore - insertCode(codeDom) + insertCode(languageType, code) } // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
src/menus/img/upload-img.ts+31 −4 modified@@ -35,13 +35,40 @@ class UploadImg { return editor.i18next.t(prefix + text) } - // 设置图片alt - const altText = alt ? `alt="${alt}" ` : '' - const hrefText = href ? `data-href="${encodeURIComponent(href)}" ` : '' + /** + * fix: insertImg xss + */ + + // 过滤src, 防止xss + let resultSrc = src.replace(/</g, '<').replace(/>/g, '>') + + // 因为下面要单引号拼接字符串, 所以要将单引号替换成双引号 + resultSrc = resultSrc.replace("'", '"') + + let hrefText = '' + + // 设置图片的元数据 data- + if (href) { + hrefText = href.replace("'", '"') + + hrefText = `data-href='${encodeURIComponent(hrefText)}' ` + } + + let altText = '' + // 设置图片alt, 过滤xss标签攻击 + if (alt) { + altText = alt.replace(/</g, '<').replace(/>/g, '>') + + // 因为下面要单引号拼接字符串, 所以要将单引号替换成双引号 + altText = altText.replace("'", '"') + + altText = `alt='${altText}' ` + } + // 先插入图片,无论是否能成功 editor.cmd.do( 'insertHTML', - `<img src="${src}" ${altText}${hrefText}style="max-width:100%;" contenteditable="false"/>` + `<img src='${resultSrc}' ${altText}${hrefText}style="max-width:100%;" contenteditable="false"/>` ) // 执行回调函数 config.linkImgCallback(src, alt, href)
src/menus/link/create-panel-conf.ts+44 −4 modified@@ -53,11 +53,16 @@ export default function (editor: Editor, text: string, link: string): PanelConf * * 同上,列表无法插入链接的原因,是因为在insertLink, 处理text时有问题。 */ + const resultText = text.replace(/</g, '<').replace(/>/g, '>') // Link xss - const $elem: DomElement = $(`<a href="${link}" target="_blank">${text}</a>`) + const $elem: DomElement = $(`<a target="_blank">${resultText}</a>`) + const linkDom = $elem.elems[0] as HTMLAnchorElement // fix: 字符转义问题,https://xxx.org?bar=1¯o=2 => https://xxx.org?bar=1¯o=2 - $elem.elems[0].innerText = text + linkDom.innerText = text + + // 避免拼接字符串,带来的字符串嵌套问题:如: <a href=""><img src=1 xx />"> 造成xss攻击 + linkDom.href = link if (isActive(editor)) { // 选区处于链接中,则选中整个菜单,再执行 insertHTML @@ -132,6 +137,9 @@ export default function (editor: Editor, text: string, link: string): PanelConf width: 300, height: 0, + // 拼接字符串的:xss 攻击: + // 如值为:"><img src=1 onerror=alert(/xss/)>, 插入后:value=""><img src=1 onerror=alert(/xss/)>", 插入一个img元素 + // panel 中可包含多个 tab tabs: [ { @@ -143,14 +151,12 @@ export default function (editor: Editor, text: string, link: string): PanelConf id="${inputTextId}" type="text" class="block" - value="${text}" placeholder="${editor.i18next.t('menus.panelMenus.link.链接文字')}"/> </td> <input id="${inputLinkId}" type="text" class="block" - value="${link}" placeholder="${editor.i18next.t('如')} https://..."/> </td> <div class="w-e-button-container"> @@ -222,6 +228,7 @@ export default function (editor: Editor, text: string, link: string): PanelConf // 选区范围是a标签,直接替换href链接即可 if ($elem?.nodeName === 'A') { $elem.setAttribute('href', link) + $elem.innerText = text return true } @@ -232,8 +239,12 @@ export default function (editor: Editor, text: string, link: string): PanelConf // 防止第一次设置就为特殊元素,这种情况应该为首次设置链接 if (nodeA) { + // 链接设置a nodeA.setAttribute('href', link) + // 文案还是要设置刚开始的元素内的文字,比如加粗的元素,不然会将加粗替代 + $elem.innerText = text + return true } } @@ -261,6 +272,35 @@ export default function (editor: Editor, text: string, link: string): PanelConf ], }, // tab end ], // tabs end + /** + * 设置input的值,分别为文案和链接地址设置值 + * + * 利用dom 设置链接文案的值,防止回填拼接引号问题, 出现xss攻击 + * + * @param $container 对应上面生成的dom容器 + * @param type text | link + */ + setLinkValue($container: DomElement, type: string) { + let inputId = '' + let inputValue = '' + let inputDom + + // 设置链接文案 + if (type === 'text') { + inputId = `#${inputTextId}` + inputValue = text + } + + // 这只链接地址 + if (type === 'link') { + inputId = `#${inputLinkId}` + inputValue = link + } + + inputDom = $container.find(inputId).elems[0] as HTMLInputElement + + inputDom.value = inputValue + }, } return conf
src/menus/link/index.ts+1 −1 modified@@ -73,7 +73,7 @@ class Link extends PanelMenu implements MenuActive { $linkElem = $(parentNodeA) } - text = $linkElem.text() + text = $linkElem.elems[0].innerText href = $linkElem.attr('href') // 弹出 panel
src/menus/menu-constructors/Panel.ts+5 −0 modified@@ -23,6 +23,7 @@ export type PanelConf = { width: number | 0 height: number | 0 tabs: PanelTabConf[] + setLinkValue?: ($container: DomElement, type: string) => void } class Panel { @@ -160,6 +161,10 @@ class Panel { // 添加到 DOM menu.$elem.append($container) + // 设置tab内input的值 + conf.setLinkValue && conf.setLinkValue($container, 'text') + conf.setLinkValue && conf.setLinkValue($container, 'link') + // 绑定 conf events 的事件,只有添加到 DOM 之后才能绑定成功 tabs.forEach((tab: PanelTabConf, index: number) => { if (!tab) {
test/unit/menus/code.test.ts+42 −1 modified@@ -48,6 +48,47 @@ test('code 菜单:插入代码', () => { let html: string = txtHtml ? txtHtml : '' + // 销毁编辑器 + editor.destroy() + + // 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述 + expect(html.indexOf(`<code class="${type}">${code}</code>`)).toBeGreaterThan(0) +}) + +test('code 菜单:插入不合法的html代码, 测试xss', () => { + const editor = createEditor(document, 'div1') + const codeMenu = getMenuInstance(editor, Code) as Code + codeMenu.clickHandler() + + const panel = codeMenu.panel as Panel + const panelElem = panel.$container.elems[0] + const $panelElem = $(panelElem) // jquery 对象 + + // panel 里的 input 和 button 元素 + const $btnInsert = $panelElem.find(":button[id^='btn-ok']") // id 以 'btn-ok' 的 button + // const $btnDel = $panelElem.find(":button[id^='btn-del']") + const $language = $panelElem.find(":input[id^='select']") + const $inputText = $panelElem.find(":input[id^='input-iframe']") + + // 插入代码 + mockCmdFn(document) + const type = 'Html' + const code = '</xmp></code></pre><img src=1 onerror=alert(/xss/)>' + + $inputText.val(code) + $language.val(type) + $btnInsert.click() + + // 挂载hljstxt + editor.highlight = hljs + + let txtHtml = editor.txt.html() + + let html: string = txtHtml ? txtHtml : '' + + // 过滤后的代码 + const filterCode = code.replace(/</g, '<').replace(/>/g, '>') + // 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述 - expect(html.indexOf(`<code class="${type}"><xmp>${code}</xmp></code>`)).toBeGreaterThan(0) + expect(html.indexOf(`<code class="${type}">${filterCode}</code>`)).toBeGreaterThan(0) })
test/unit/menus/img/upload-img.test.ts+38 −2 modified@@ -73,6 +73,42 @@ describe('upload img', () => { mockImgOnloadAndOnError() }) + test('调用 insertImg 利用html拼接,在url里插入xss攻击的代码', () => { + const uploadImg = createUploadImgInstance() + + // 根据源码拼接字符串的xss攻击 + const imgUrl = '"><img src=1 onerror=alert(/xss/)>' + let resultSrc = imgUrl.replace(/</g, '<').replace(/>/g, '>') + + mockSupportCommand() + + uploadImg.insertImg(imgUrl) + + expect(document.execCommand).toBeCalledWith( + 'insertHTML', + false, + `<img src='${resultSrc}' style="max-width:100%;" contenteditable="false"/>` + ) + }) + + test('调用 insertImg 利用html拼接,在alt里插入xss攻击的代码', () => { + const uploadImg = createUploadImgInstance() + + // 根据源码拼接字符串的xss攻击 + const imgAlt = '"><img src=1 onerror=alert(/xss/)>' + let resultAlt = imgAlt.replace(/</g, '<').replace(/>/g, '>') + + mockSupportCommand() + + uploadImg.insertImg(imgUrl, imgAlt) + + expect(document.execCommand).toBeCalledWith( + 'insertHTML', + false, + `<img src='${imgUrl}' alt='${resultAlt}' style="max-width:100%;" contenteditable="false"/>` + ) + }) + test('调用 insertImg 可以网编辑器里插入图片', () => { const uploadImg = createUploadImgInstance() @@ -83,7 +119,7 @@ describe('upload img', () => { expect(document.execCommand).toBeCalledWith( 'insertHTML', false, - `<img src="${imgUrl}" style="max-width:100%;" contenteditable="false"/>` + `<img src='${imgUrl}' style="max-width:100%;" contenteditable="false"/>` ) }) @@ -101,7 +137,7 @@ describe('upload img', () => { expect(document.execCommand).toBeCalledWith( 'insertHTML', false, - `<img src="${imgUrl}" style="max-width:100%;" contenteditable="false"/>` + `<img src='${imgUrl}' style="max-width:100%;" contenteditable="false"/>` ) expect(callback).toBeCalledWith(imgUrl, undefined, undefined) })
test/unit/menus/link/link.test.ts+1 −1 modified@@ -43,7 +43,7 @@ describe('link菜单', () => { // 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述 expect( - editor.$textElem.html().indexOf(`<a href="${link}" target="_blank">${text}</a>`) + editor.$textElem.html().indexOf(`<a target="_blank" href="${link}">${text}</a>`) ).toBeGreaterThan(0) }) })
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-9hfw-cvf4-5x25ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-25037ghsaADVISORY
- gist.github.com/Mdxjj/5cf0a31e8abf24ed688ceb5b3543516dnvdWEB
- github.com/wangeditor-team/wangEditor/commit/6257a2e166346913c34ac5cfb31b6a46e9544c5aghsaWEB
- github.com/wangeditor-team/wangEditor/issues/3870nvdWEB
- github.com/wangeditor-team/wangEditor/issues/3872ghsaWEB
News mentions
0No linked articles in our index yet.