Skip to content

Commit

Permalink
feat: xss
Browse files Browse the repository at this point in the history
  • Loading branch information
Gavin-yh committed Feb 23, 2022
1 parent 6e30856 commit 6257a2e
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 28 deletions.
29 changes: 14 additions & 15 deletions src/menus/code/create-panel-conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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, '&lt;').replace(/>/g, '&gt;')

// 高亮渲染
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)
Expand Down Expand Up @@ -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 不会关闭
Expand Down
35 changes: 31 additions & 4 deletions src/menus/img/upload-img.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '&lt;').replace(/>/g, '&gt;')

// 因为下面要单引号拼接字符串, 所以要将单引号替换成双引号
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, '&lt;').replace(/>/g, '&gt;')

// 因为下面要单引号拼接字符串, 所以要将单引号替换成双引号
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)
Expand Down
48 changes: 44 additions & 4 deletions src/menus/link/create-panel-conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ export default function (editor: Editor, text: string, link: string): PanelConf
*
* 同上,列表无法插入链接的原因,是因为在insertLink, 处理text时有问题。
*/
const resultText = text.replace(/</g, '&lt;').replace(/>/g, '&gt;') // 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&macro=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
Expand Down Expand Up @@ -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: [
{
Expand All @@ -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">
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/menus/link/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/menus/menu-constructors/Panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type PanelConf = {
width: number | 0
height: number | 0
tabs: PanelTabConf[]
setLinkValue?: ($container: DomElement, type: string) => void
}

class Panel {
Expand Down Expand Up @@ -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) {
Expand Down
43 changes: 42 additions & 1 deletion test/unit/menus/code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '&lt;').replace(/>/g, '&gt;')

// 此处触发 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)
})
40 changes: 38 additions & 2 deletions test/unit/menus/img/upload-img.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '&lt;').replace(/>/g, '&gt;')

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, '&lt;').replace(/>/g, '&gt;')

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()

Expand All @@ -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"/>`
)
})

Expand All @@ -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)
})
Expand Down
2 changes: 1 addition & 1 deletion test/unit/menus/link/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})

0 comments on commit 6257a2e

Please sign in to comment.