Vulnerability Detail Report
Vulnerability Overview
- ZDID: ZD-2021-00237
- Vendor: Matters Lab
- Title: [Bounty] Matters Lab XSS
- Introduction: XSS
處理狀態
目前狀態
-
新提交
-
已審核
-
已通報
-
已修補
-
未複測
-
公開
處理歷程
- 2021/05/04 22:06:35 : 新提交 (由 huli 更新此狀態)
- 2022/02/18 14:13:02 : 新提交 (由 huli 更新此狀態)
- 2022/02/18 14:14:41 : 新提交 (由 huli 更新此狀態)
- 2022/02/23 07:43:30 : 已修補 (由 組織帳號 更新此狀態)
- 2022/02/23 07:45:26 : 公開 (由 組織帳號 更新此狀態)
- 2022/02/24 03:00:01 : 公開 (由 HITCON ZeroDay 平台自動更新)
詳細資料
- ZDID:ZD-2021-00237
- 通報者:aszx87410 (huli)
- 風險:低
- 類型:預存式跨站腳本攻擊 (Stored Cross-Site Scripting)
參考資料
漏洞說明: OWASP - Cross-site Scripting (XSS)
https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)
防護原則:
https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
XSS 防禦繞過方式:
https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
相關網址
敘述
在文章顯示部份有 XSS 漏洞,可以讓瀏覽文章的人都觸發 XSS。
這邊為了給出 POC 但不影響一般使用者,改用 console.log 取代 alert,打開 devtool 可看到 log 1337 即為 XSS 觸發
漏洞發現過程如下:
對於文章的顯示,在 server 有先做過 sanitize(src/common/utils/xss.ts)
import xss from 'xss'
const CUSTOM_WHITE_LISTS = {
a: [...(xss.whiteList.a || []), 'class'],
figure: [],
figcaption: [],
source: ['src', 'type'],
iframe: ['src', 'frameborder', 'allowfullscreen', 'sandbox'],
}
const onIgnoreTagAttr = (tag: string, name: string, value: string) => {
/**
* Allow attributes of whitelist tags start with "data-" or "class"
*
* @see https://github.com/leizongmin/js-xss#allow-attributes-of-whitelist-tags-start-with-data-
*/
if (name.substr(0, 5) === 'data-' || name.substr(0, 5) === 'class') {
// escape its value using built-in escapeAttrValue function
return name + '="' + xss.escapeAttrValue(value) + '"'
}
}
const ignoreTagProcessor = (
tag: string,
html: string,
options: { [key: string]: any }
) => {
if (tag === 'input' || tag === 'textarea') {
return ''
}
}
const xssOptions = {
whiteList: { ...xss.whiteList, ...CUSTOM_WHITE_LISTS },
onIgnoreTagAttr,
onIgnoreTag: ignoreTagProcessor,
}
const customXSS = new xss.FilterXSS(xssOptions)
export const sanitize = (string: string) => customXSS.process(string)
可是在 client 的時候,render 文章時會再傳進一個 optimizeEmbed 的 function( src/views/ArticleDetail/Content/index.tsx)
return (
<>
<div
className={classNames({ 'u-content': true, translating })}
dangerouslySetInnerHTML=
__html: optimizeEmbed(translation || article.content),
onClick={captureClicks}
ref={contentContainer}
/>
<style jsx>{styles}</style>
</>
)
繼續往下追,看到 src/common/utils/text.ts:
export const optimizeEmbed = (content: string) => {
return content
.replace(/\<iframe /g, '<iframe loading="lazy"')
.replace(
/<img\s[^>]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/g,
(match, src, offset) => {
return /* html */ `
<picture>
<source
type="image/webp"
media="(min-width: 768px)"
srcSet=${toSizedImageURL({ url: src, size: '1080w', ext: 'webp' })}
onerror="this.srcset='${src}'"
/>
<source
media="(min-width: 768px)"
srcSet=${toSizedImageURL({ url: src, size: '1080w' })}
onerror="this.srcset='${src}'"
/>
<source
type="image/webp"
srcSet=${toSizedImageURL({ url: src, size: '540w', ext: 'webp' })}
/>
<img
src=${src}
srcSet=${toSizedImageURL({ url: src, size: '540w' })}
loading="lazy"
/>
</picture>
`
}
)
}
這邊採用 RegExp 把 img src 拿出來,然後用字串拼接的方式直接拼成 HTML,看起來不太安全,再往下看 toSizedImageURL:
export const toSizedImageURL = ({ url, size, ext }: ToSizedImageURLProps) => {
const assetDomain = process.env.NEXT_PUBLIC_ASSET_DOMAIN
? `https://${process.env.NEXT_PUBLIC_ASSET_DOMAIN}`
: ''
const isOutsideLink = url.indexOf(assetDomain) < 0
const isGIF = /gif/i.test(url)
if (!assetDomain || isOutsideLink || isGIF) {
return url
}
const key = url.replace(assetDomain, ``)
const extedUrl = changeExt({ key, ext })
const prefix = size ? '/' + PROCESSED_PREFIX + '/' + size : ''
return assetDomain + prefix + extedUrl
}
只要 domain 是 assets 的 domain 並符合其他條件,就會經過一些字串處理之後回傳。
重點可以放回剛剛 source 那邊,其中一段程式碼為:
<source
type="image/webp"
media="(min-width: 768px)"
srcSet=${toSizedImageURL({ url: src, size: '1080w', ext: 'webp' })}
onerror="this.srcset='${src}'"
/>
如果 ${toSizedImageURL({ url: src, size: '1080w', ext: 'webp' })} 這段我們可以控制的話,就可以用空格插入新屬性,然後 onerror 其實是無效的,這邊可以用 onanimationstart 搭配網站上找到的 keyframe: spinning。
因此如果 img src 為:https://assets.matters.news/processed/1080w/embed/test style=animation-name:spinning onanimationstart=console.log(1337)
結合後的程式碼就是:
<source
type="image/webp"
media="(min-width: 768px)"
srcSet=https://assets.matters.news/processed/1080w/embed/test
style=animation-name:spinning
onanimationstart=console.log(1337)
onerror="this.srcset='${src}'"
/>
就製造了一個 XSS 漏洞。
修補建議
建議不要採用字串拼接搭配 dangerouslySetInnerHTML,一率使用 xss 相關 library 來防護