Skip to content

Commit 6257a2e

Browse files
committed
feat: xss
1 parent 6e30856 commit 6257a2e

File tree

8 files changed

+176
-28
lines changed

8 files changed

+176
-28
lines changed

src/menus/code/create-panel-conf.ts

+14-15
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function (editor: Editor, text: string, languageType: string): Pa
2020
* 插入代码块
2121
* @param text 文字
2222
*/
23-
function insertCode(text: string): void {
23+
function insertCode(languateType: string, code: string): void {
2424
// 选区处于链接中,则选中整个菜单,再执行 insertHTML
2525
let active = isActive(editor)
2626

@@ -29,10 +29,21 @@ export default function (editor: Editor, text: string, languageType: string): Pa
2929
}
3030

3131
const content = editor.selection.getSelectionStartElem()?.elems[0].innerHTML
32+
3233
if (content) {
3334
editor.cmd.do('insertHTML', EMPTY_P)
3435
}
35-
editor.cmd.do('insertHTML', text)
36+
37+
// 过滤标签,防止xss
38+
let formatCode = code.replace(/</g, '&lt;').replace(/>/g, '&gt;')
39+
40+
// 高亮渲染
41+
if (editor.highlight) {
42+
formatCode = editor.highlight.highlightAuto(formatCode).value
43+
}
44+
45+
//增加pre标签
46+
editor.cmd.do('insertHTML', `<pre><code class="${languateType}">${formatCode}</code></pre>`)
3647

3748
const $code = editor.selection.getSelectionStartElem()
3849
const $codeElem = $code?.getNodeTop(editor)
@@ -109,34 +120,22 @@ export default function (editor: Editor, text: string, languageType: string): Pa
109120
selector: '#' + btnOkId,
110121
type: 'click',
111122
fn: () => {
112-
let formatCode, codeDom
113-
114123
const $code = document.getElementById(inputIFrameId)
115124
const $select = $('#' + languageId)
116125

117126
let languageType = $select.val()
118127
// @ts-ignore
119128
let code = $code.value
120129

121-
// 高亮渲染
122-
if (editor.highlight) {
123-
formatCode = editor.highlight.highlightAuto(code).value
124-
} else {
125-
formatCode = `<xmp>${code}</xmp>`
126-
}
127-
128130
// 代码为空,则不插入
129131
if (!code) return
130132

131133
//增加标签
132134
if (isActive(editor)) {
133135
return false
134136
} else {
135-
//增加pre标签
136-
codeDom = `<pre><code class="${languageType}">${formatCode}</code></pre>`
137-
138137
// @ts-ignore
139-
insertCode(codeDom)
138+
insertCode(languageType, code)
140139
}
141140

142141
// 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭

src/menus/img/upload-img.ts

+31-4
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,40 @@ class UploadImg {
3535
return editor.i18next.t(prefix + text)
3636
}
3737

38-
// 设置图片alt
39-
const altText = alt ? `alt="${alt}" ` : ''
40-
const hrefText = href ? `data-href="${encodeURIComponent(href)}" ` : ''
38+
/**
39+
* fix: insertImg xss
40+
*/
41+
42+
// 过滤src, 防止xss
43+
let resultSrc = src.replace(/</g, '&lt;').replace(/>/g, '&gt;')
44+
45+
// 因为下面要单引号拼接字符串, 所以要将单引号替换成双引号
46+
resultSrc = resultSrc.replace("'", '"')
47+
48+
let hrefText = ''
49+
50+
// 设置图片的元数据 data-
51+
if (href) {
52+
hrefText = href.replace("'", '"')
53+
54+
hrefText = `data-href='${encodeURIComponent(hrefText)}' `
55+
}
56+
57+
let altText = ''
58+
// 设置图片alt, 过滤xss标签攻击
59+
if (alt) {
60+
altText = alt.replace(/</g, '&lt;').replace(/>/g, '&gt;')
61+
62+
// 因为下面要单引号拼接字符串, 所以要将单引号替换成双引号
63+
altText = altText.replace("'", '"')
64+
65+
altText = `alt='${altText}' `
66+
}
67+
4168
// 先插入图片,无论是否能成功
4269
editor.cmd.do(
4370
'insertHTML',
44-
`<img src="${src}" ${altText}${hrefText}style="max-width:100%;" contenteditable="false"/>`
71+
`<img src='${resultSrc}' ${altText}${hrefText}style="max-width:100%;" contenteditable="false"/>`
4572
)
4673
// 执行回调函数
4774
config.linkImgCallback(src, alt, href)

src/menus/link/create-panel-conf.ts

+44-4
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,16 @@ export default function (editor: Editor, text: string, link: string): PanelConf
5353
*
5454
* 同上,列表无法插入链接的原因,是因为在insertLink, 处理text时有问题。
5555
*/
56+
const resultText = text.replace(/</g, '&lt;').replace(/>/g, '&gt;') // Link xss
5657

57-
const $elem: DomElement = $(`<a href="${link}" target="_blank">${text}</a>`)
58+
const $elem: DomElement = $(`<a target="_blank">${resultText}</a>`)
59+
const linkDom = $elem.elems[0] as HTMLAnchorElement
5860

5961
// fix: 字符转义问题,https://xxx.org?bar=1&macro=2 => https://xxx.org?bar=1¯o=2
60-
$elem.elems[0].innerText = text
62+
linkDom.innerText = text
63+
64+
// 避免拼接字符串,带来的字符串嵌套问题:如: <a href=""><img src=1 xx />"> 造成xss攻击
65+
linkDom.href = link
6166

6267
if (isActive(editor)) {
6368
// 选区处于链接中,则选中整个菜单,再执行 insertHTML
@@ -132,6 +137,9 @@ export default function (editor: Editor, text: string, link: string): PanelConf
132137
width: 300,
133138
height: 0,
134139

140+
// 拼接字符串的:xss 攻击:
141+
// 如值为:"><img src=1 onerror=alert(/xss/)>, 插入后:value=""><img src=1 onerror=alert(/xss/)>", 插入一个img元素
142+
135143
// panel 中可包含多个 tab
136144
tabs: [
137145
{
@@ -143,14 +151,12 @@ export default function (editor: Editor, text: string, link: string): PanelConf
143151
id="${inputTextId}"
144152
type="text"
145153
class="block"
146-
value="${text}"
147154
placeholder="${editor.i18next.t('menus.panelMenus.link.链接文字')}"/>
148155
</td>
149156
<input
150157
id="${inputLinkId}"
151158
type="text"
152159
class="block"
153-
value="${link}"
154160
placeholder="${editor.i18next.t('如')} https://..."/>
155161
</td>
156162
<div class="w-e-button-container">
@@ -222,6 +228,7 @@ export default function (editor: Editor, text: string, link: string): PanelConf
222228
// 选区范围是a标签,直接替换href链接即可
223229
if ($elem?.nodeName === 'A') {
224230
$elem.setAttribute('href', link)
231+
$elem.innerText = text
225232

226233
return true
227234
}
@@ -232,8 +239,12 @@ export default function (editor: Editor, text: string, link: string): PanelConf
232239

233240
// 防止第一次设置就为特殊元素,这种情况应该为首次设置链接
234241
if (nodeA) {
242+
// 链接设置a
235243
nodeA.setAttribute('href', link)
236244

245+
// 文案还是要设置刚开始的元素内的文字,比如加粗的元素,不然会将加粗替代
246+
$elem.innerText = text
247+
237248
return true
238249
}
239250
}
@@ -261,6 +272,35 @@ export default function (editor: Editor, text: string, link: string): PanelConf
261272
],
262273
}, // tab end
263274
], // tabs end
275+
/**
276+
* 设置input的值,分别为文案和链接地址设置值
277+
*
278+
* 利用dom 设置链接文案的值,防止回填拼接引号问题, 出现xss攻击
279+
*
280+
* @param $container 对应上面生成的dom容器
281+
* @param type text | link
282+
*/
283+
setLinkValue($container: DomElement, type: string) {
284+
let inputId = ''
285+
let inputValue = ''
286+
let inputDom
287+
288+
// 设置链接文案
289+
if (type === 'text') {
290+
inputId = `#${inputTextId}`
291+
inputValue = text
292+
}
293+
294+
// 这只链接地址
295+
if (type === 'link') {
296+
inputId = `#${inputLinkId}`
297+
inputValue = link
298+
}
299+
300+
inputDom = $container.find(inputId).elems[0] as HTMLInputElement
301+
302+
inputDom.value = inputValue
303+
},
264304
}
265305

266306
return conf

src/menus/link/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class Link extends PanelMenu implements MenuActive {
7373
$linkElem = $(parentNodeA)
7474
}
7575

76-
text = $linkElem.text()
76+
text = $linkElem.elems[0].innerText
7777
href = $linkElem.attr('href')
7878

7979
// 弹出 panel

src/menus/menu-constructors/Panel.ts

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type PanelConf = {
2323
width: number | 0
2424
height: number | 0
2525
tabs: PanelTabConf[]
26+
setLinkValue?: ($container: DomElement, type: string) => void
2627
}
2728

2829
class Panel {
@@ -160,6 +161,10 @@ class Panel {
160161
// 添加到 DOM
161162
menu.$elem.append($container)
162163

164+
// 设置tab内input的值
165+
conf.setLinkValue && conf.setLinkValue($container, 'text')
166+
conf.setLinkValue && conf.setLinkValue($container, 'link')
167+
163168
// 绑定 conf events 的事件,只有添加到 DOM 之后才能绑定成功
164169
tabs.forEach((tab: PanelTabConf, index: number) => {
165170
if (!tab) {

test/unit/menus/code.test.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,47 @@ test('code 菜单:插入代码', () => {
4848

4949
let html: string = txtHtml ? txtHtml : ''
5050

51+
// 销毁编辑器
52+
editor.destroy()
53+
54+
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
55+
expect(html.indexOf(`<code class="${type}">${code}</code>`)).toBeGreaterThan(0)
56+
})
57+
58+
test('code 菜单:插入不合法的html代码, 测试xss', () => {
59+
const editor = createEditor(document, 'div1')
60+
const codeMenu = getMenuInstance(editor, Code) as Code
61+
codeMenu.clickHandler()
62+
63+
const panel = codeMenu.panel as Panel
64+
const panelElem = panel.$container.elems[0]
65+
const $panelElem = $(panelElem) // jquery 对象
66+
67+
// panel 里的 input 和 button 元素
68+
const $btnInsert = $panelElem.find(":button[id^='btn-ok']") // id 以 'btn-ok' 的 button
69+
// const $btnDel = $panelElem.find(":button[id^='btn-del']")
70+
const $language = $panelElem.find(":input[id^='select']")
71+
const $inputText = $panelElem.find(":input[id^='input-iframe']")
72+
73+
// 插入代码
74+
mockCmdFn(document)
75+
const type = 'Html'
76+
const code = '</xmp></code></pre><img src=1 onerror=alert(/xss/)>'
77+
78+
$inputText.val(code)
79+
$language.val(type)
80+
$btnInsert.click()
81+
82+
// 挂载hljstxt
83+
editor.highlight = hljs
84+
85+
let txtHtml = editor.txt.html()
86+
87+
let html: string = txtHtml ? txtHtml : ''
88+
89+
// 过滤后的代码
90+
const filterCode = code.replace(/</g, '&lt;').replace(/>/g, '&gt;')
91+
5192
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
52-
expect(html.indexOf(`<code class="${type}"><xmp>${code}</xmp></code>`)).toBeGreaterThan(0)
93+
expect(html.indexOf(`<code class="${type}">${filterCode}</code>`)).toBeGreaterThan(0)
5394
})

test/unit/menus/img/upload-img.test.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,42 @@ describe('upload img', () => {
7373
mockImgOnloadAndOnError()
7474
})
7575

76+
test('调用 insertImg 利用html拼接,在url里插入xss攻击的代码', () => {
77+
const uploadImg = createUploadImgInstance()
78+
79+
// 根据源码拼接字符串的xss攻击
80+
const imgUrl = '"><img src=1 onerror=alert(/xss/)>'
81+
let resultSrc = imgUrl.replace(/</g, '&lt;').replace(/>/g, '&gt;')
82+
83+
mockSupportCommand()
84+
85+
uploadImg.insertImg(imgUrl)
86+
87+
expect(document.execCommand).toBeCalledWith(
88+
'insertHTML',
89+
false,
90+
`<img src='${resultSrc}' style="max-width:100%;" contenteditable="false"/>`
91+
)
92+
})
93+
94+
test('调用 insertImg 利用html拼接,在alt里插入xss攻击的代码', () => {
95+
const uploadImg = createUploadImgInstance()
96+
97+
// 根据源码拼接字符串的xss攻击
98+
const imgAlt = '"><img src=1 onerror=alert(/xss/)>'
99+
let resultAlt = imgAlt.replace(/</g, '&lt;').replace(/>/g, '&gt;')
100+
101+
mockSupportCommand()
102+
103+
uploadImg.insertImg(imgUrl, imgAlt)
104+
105+
expect(document.execCommand).toBeCalledWith(
106+
'insertHTML',
107+
false,
108+
`<img src='${imgUrl}' alt='${resultAlt}' style="max-width:100%;" contenteditable="false"/>`
109+
)
110+
})
111+
76112
test('调用 insertImg 可以网编辑器里插入图片', () => {
77113
const uploadImg = createUploadImgInstance()
78114

@@ -83,7 +119,7 @@ describe('upload img', () => {
83119
expect(document.execCommand).toBeCalledWith(
84120
'insertHTML',
85121
false,
86-
`<img src="${imgUrl}" style="max-width:100%;" contenteditable="false"/>`
122+
`<img src='${imgUrl}' style="max-width:100%;" contenteditable="false"/>`
87123
)
88124
})
89125

@@ -101,7 +137,7 @@ describe('upload img', () => {
101137
expect(document.execCommand).toBeCalledWith(
102138
'insertHTML',
103139
false,
104-
`<img src="${imgUrl}" style="max-width:100%;" contenteditable="false"/>`
140+
`<img src='${imgUrl}' style="max-width:100%;" contenteditable="false"/>`
105141
)
106142
expect(callback).toBeCalledWith(imgUrl, undefined, undefined)
107143
})

test/unit/menus/link/link.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('link菜单', () => {
4343

4444
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
4545
expect(
46-
editor.$textElem.html().indexOf(`<a href="${link}" target="_blank">${text}</a>`)
46+
editor.$textElem.html().indexOf(`<a target="_blank" href="${link}">${text}</a>`)
4747
).toBeGreaterThan(0)
4848
})
4949
})

0 commit comments

Comments
 (0)