性防御實踐)
1. 項目概述從“收藏”到“吃透”的DOM型XSS看到“收藏”這個詞我猜很多朋友和我一樣瀏覽器里存了一堆關(guān)于安全漏洞的“深度解析”文章但真到用的時候發(fā)現(xiàn)還是云里霧里。DOM型XSS跨站腳本攻擊就是個典型它不像反射型或存儲型XSS那樣“所見即所得”它的攻擊載荷在服務(wù)器響應(yīng)里根本看不見完全在客戶端的瀏覽器里“憑空”發(fā)生。這種特性讓它成了前端安全里一個既隱蔽又棘手的難題。今天我們不搞那種羅列一堆漏洞代碼然后貼個“修復(fù)方案”的套路而是從一個前端開發(fā)者或安全測試人員的視角把DOM型XSS徹底掰開揉碎講清楚它到底是怎么“無中生有”的攻擊者有哪些腦洞大開的繞過姿勢以及我們該如何從根源上構(gòu)建防御。這篇文章的目標(biāo)就是讓你收藏之后能真正理解、識別并防御它而不僅僅是又一個“已讀”的標(biāo)簽。2. DOM型XSS的核心原理與獨特之處2.1 什么是DOM它為何成為攻擊面要理解DOM型XSS首先得拋開對“網(wǎng)頁就是一堆HTML文本”的簡單認(rèn)知。當(dāng)瀏覽器接收到服務(wù)器的HTML文檔后它會解析并構(gòu)建一個樹狀結(jié)構(gòu)這就是文檔對象模型DOM。你可以把DOM想象成瀏覽器內(nèi)存中一個動態(tài)的、可編程的網(wǎng)頁“地圖”。JavaScript可以通過這張地圖的API比如document.getElementById、innerHTML、location.hash等來讀取、修改、添加或刪除地圖上的任何“地點”即節(jié)點。DOM型XSS的根源就在于攻擊者能夠通過某種方式將惡意腳本“注入”到這個動態(tài)的DOM構(gòu)建過程中而這個過程可能完全不需要經(jīng)過服務(wù)器。舉個例子一個頁面通過location.hashURL中#后面的部分來動態(tài)決定顯示什么內(nèi)容// 假設(shè)URL是https://example.com/page.html#img srcx onerroralert(1) var userContent location.hash.substring(1); // 獲取#后面的內(nèi)容 document.getElementById(displayArea).innerHTML 您輸入的是: userContent;這段代碼的本意可能是想友好地回顯URL片段。但攻擊者構(gòu)造一個特殊的URL讓userContent包含惡意腳本。當(dāng)受害者訪問這個URL時腳本就在其瀏覽器環(huán)境中執(zhí)行了。關(guān)鍵點在于服務(wù)器響應(yīng)的原始HTML里根本沒有img srcx onerroralert(1)這段代碼。惡意載荷是作為URL的一部分直接傳遞給客戶端腳本并由客戶端腳本“寫”入DOM的。這就是DOM型XSS最核心的特征漏洞的“源”Source和“匯”Sink都在客戶端瀏覽器中。2.2 與反射型、存儲型XSS的本質(zhì)區(qū)別很多人容易混淆這里必須劃清界限。反射型XSS惡意腳本來自當(dāng)前HTTP請求通常是URL參數(shù)或表單提交服務(wù)器“反射”式地將其嵌入到返回的HTML頁面中。漏洞發(fā)生在服務(wù)器端響應(yīng)內(nèi)容的生成階段。檢查服務(wù)器返回的HTML源碼能看到攻擊載荷。存儲型XSS惡意腳本被持久化存儲在服務(wù)器端如數(shù)據(jù)庫、評論、消息當(dāng)其他用戶訪問某個頁面時腳本從服務(wù)器加載并嵌入在頁面中。漏洞也發(fā)生在服務(wù)器端輸出時。DOM型XSS如前所述惡意腳本不經(jīng)過服務(wù)器處理或者服務(wù)器返回的是靜態(tài)、無害的HTML由客戶端JavaScript邏輯在處理某些數(shù)據(jù)如URL、Cookie、本地存儲、postMessage消息時將其錯誤地寫入了DOM的“匯”點。一個簡單的鑒別方法查看網(wǎng)頁源代碼View Source。如果在源碼里看到了攻擊字符串那很可能是反射型或存儲型如果源碼很干凈但頁面依然彈出了警告框那基本就是DOM型XSS在作祟了。2.3 危險的“源”Source與“匯”Sink理解DOM型XSS必須掌握這兩個關(guān)鍵概念這是進(jìn)行代碼審計和漏洞挖掘的基礎(chǔ)思維模型。源Source指那些可以被攻擊者控制的數(shù)據(jù)輸入點。常見的源包括document.URL/location對象href,hash,searchdocument.referrer來源頁面URLdocument.cookiewindow.namelocalStorage/sessionStoragepostMessage消息內(nèi)容通過URLSearchParams或FormData獲取的參數(shù)匯Sink指那些能將字符串?dāng)?shù)據(jù)作為HTML或JavaScript代碼來解析和執(zhí)行的DOM屬性或方法。將來自“源”的不受信數(shù)據(jù)傳遞給“匯”就可能導(dǎo)致XSS。高危的匯主要有幾類HTML注入?yún)R點直接將字符串當(dāng)作HTML解析。element.innerHTMLelement.outerHTMLdocument.write()/document.writeln()某些特殊屬性如iframe.srcdoc腳本執(zhí)行匯點將字符串當(dāng)作JavaScript代碼執(zhí)行。eval()setTimeout()/setInterval()第一個參數(shù)為字符串時new Function()location.href javascript:...偽協(xié)議跳轉(zhuǎn)/資源加載匯點可能導(dǎo)致腳本執(zhí)行或敏感信息泄露。location.href/location.assign()/location.replace()a.href當(dāng)設(shè)置為javascript:或可控的URL時script.src,iframe.src,img.src可能造成盲打或結(jié)合SVG等格式執(zhí)行腳本實操心得在代碼審查時我習(xí)慣性地用“源-匯”流向來思考。每當(dāng)看到一個“匯”方法被調(diào)用立刻向上追蹤它的參數(shù)來源。如果這個參數(shù)鏈條最終能追溯到任何一個“源”并且中間沒有經(jīng)過恰當(dāng)?shù)膬艋敲催@里就存在一個潛在的DOM型XSS漏洞。自動化工具如SAST也是基于這種數(shù)據(jù)流分析原理來工作的。3. 攻擊向量與高級繞過技巧實錄攻擊者不會只滿足于alert(1)。在實際的滲透測試或真實攻擊中他們會利用各種技巧來繞過防御、隱藏攻擊意圖。3.1 經(jīng)典攻擊向量剖析基于URL片段的攻擊如前所述利用location.hash是最常見的一種。因為hash不會發(fā)送到服務(wù)器所以傳統(tǒng)的WAFWeb應(yīng)用防火墻和服務(wù)器端日志完全無法檢測。攻擊鏈非常直接誘騙用戶點擊一個特制的鏈接。基于postMessage的跨域攻擊現(xiàn)代Web應(yīng)用大量使用iframe和跨域通信。如果接收postMessage的頁面沒有嚴(yán)格校驗消息來源event.origin并且直接將消息內(nèi)容傳遞給innerHTML或eval()攻擊者就可以從任何一個惡意頁面向該應(yīng)用發(fā)送惡意腳本并執(zhí)行。利用第三方庫或框架的缺陷很多前端框架或庫在提供動態(tài)內(nèi)容渲染功能時如果使用不當(dāng)會成為DOM型XSS的入口。例如早期某些模板引擎在解析特定語法時可能意外地將用戶輸入當(dāng)作表達(dá)式執(zhí)行。審計時需要特別關(guān)注那些將數(shù)據(jù)綁定到v-htmlVue或dangerouslySetInnerHTMLReact等危險指令的場景。3.2 繞過輸入過濾與編碼這是攻防對抗最激烈的地方。假設(shè)開發(fā)者在將數(shù)據(jù)放入innerHTML前嘗試用replace(‘’, ‘lt;’)過濾了尖括號。繞過尖括號轉(zhuǎn)義如果只是過濾了和攻擊者可能根本不需要它們。許多HTML標(biāo)簽的屬性本身就可以執(zhí)行JavaScript。!-- 利用圖片標(biāo)簽的onerror屬性 -- img srcx onerroralert(1) !-- 利用svg標(biāo)簽其內(nèi)部允許腳本 -- svg onloadalert(1) !-- 利用事件處理器如onmouseover不需要尖括號閉合 -- “ onmouseover”alert(1)如果輸入被直接插入到某個現(xiàn)有標(biāo)簽的屬性中例如input value”USER_INPUT”那么攻擊者輸入” onfocus”alert(1) autofocus”就會閉合前一個屬性插入新的事件處理器并自動觸發(fā)。最終構(gòu)造出的DOM是input value”” onfocus”alert(1)” autofocus””。利用JavaScript上下文如果數(shù)據(jù)是被插入到script標(biāo)簽內(nèi)部或者eval()、setTimeout()的字符串參數(shù)中過濾HTML實體就無效了。攻擊者需要閉合當(dāng)前的JS字符串或語句。例如var data “USER_INPUT”; // 假設(shè)用戶輸入是 ”; alert(1); //最終代碼變?yōu)関ar data “”; alert(1); //“;成功注入。編碼嵌套與混淆瀏覽器解析是有順序的。假設(shè)服務(wù)器對輸入進(jìn)行了HTML編碼變成lt;然后前端JS又進(jìn)行了一次解碼如用了innerHTML瀏覽器會自動解碼HTML實體那么攻擊載荷可能被還原。更復(fù)雜的可以利用JS編碼如Unicode轉(zhuǎn)義\u003c、URL編碼等在數(shù)據(jù)流動的不同階段進(jìn)行混淆以繞過簡單的黑名單檢測。注意事項防御DOM型XSS絕不能依賴簡單的、基于黑名單的字符替換。這種思路永遠(yuǎn)落后于攻擊者的創(chuàng)造力。我曾見過一個案例過濾器過濾了script、onerror等關(guān)鍵詞攻擊者使用了scrscriptipt試圖繞過但開發(fā)者又寫了一個循環(huán)替換把中間的script去掉后兩邊拼起來又成了script。這變成了一個永無休止的貓鼠游戲。正確的思路是白名單和上下文相關(guān)的編碼。4. 系統(tǒng)性防御從根源上杜絕漏洞防御DOM型XSS需要一套組合拳涵蓋開發(fā)規(guī)范、安全編碼、庫的使用和測試流程。4.1 第一原則避免使用危險的“匯”最有效的防御就是不用。在代碼設(shè)計和評審時就要挑戰(zhàn)每一個“匯”的使用必要性。用textContent代替innerHTML如果目的只是顯示文本絕對不要使用innerHTML。textContent屬性會將輸入純文本化徹底杜絕HTML解析。避免eval()、new Function()和字符串參數(shù)的setTimeout在現(xiàn)代前端開發(fā)中幾乎沒有必須使用eval的場景。動態(tài)代碼執(zhí)行的需求可以通過設(shè)計更好的函數(shù)或模塊結(jié)構(gòu)來解決。謹(jǐn)慎使用document.write()這個方法已經(jīng)非常古老且會阻塞頁面解析。在動態(tài)內(nèi)容加載的場景下應(yīng)使用DOM API如appendChild或現(xiàn)代框架的聲明式渲染來替代。4.2 第二原則實施上下文相關(guān)的輸出編碼如果必須使用危險的“匯”比如需要渲染富文本那么必須在數(shù)據(jù)從“源”流向“匯”的路徑上根據(jù)最終的“上下文”進(jìn)行正確的編碼。對于HTML上下文如果數(shù)據(jù)要插入到HTML標(biāo)簽之間如divUSER_DATA/div需要對,,,”,’進(jìn)行HTML實體編碼。但如果是插入到標(biāo)簽屬性里如input value”USER_DATA”除了上述字符空格和引號也可能被利用需要更嚴(yán)格的編碼。對于JavaScript上下文如果數(shù)據(jù)要插入到script標(biāo)簽內(nèi)或作為JS字符串字面量需要對其進(jìn)行JavaScript Unicode轉(zhuǎn)義確保它被當(dāng)作一個完整的字符串令牌而不是可執(zhí)行的代碼。對于URL上下文如果數(shù)據(jù)要作為URL的一部分如href、src必須使用encodeURIComponent進(jìn)行完整的URL編碼。這里有一個關(guān)鍵陷阱很多人知道用encodeURI或encodeURIComponent但用錯了地方。encodeURI用于編碼整個URI它不會對/,?,等保留字符編碼不適合編碼參數(shù)值。而encodeURIComponent才是用于編碼URI組件如參數(shù)值的它更徹底。但請注意如果你要把編碼后的值放入HTML屬性里你實際上需要的是HTML編碼而不是URL編碼?;煜舷挛氖菍?dǎo)致防御失效的常見原因。4.3 第三原則使用經(jīng)過安全驗證的庫和框架不要自己重復(fù)造輪子尤其是安全輪子。使用安全的DOM API如DOMPurify。這是一個非常健壯的HTML清理庫。它的原理是先在一個沙盒化的DOM樹中解析你的HTML字符串然后根據(jù)一個嚴(yán)格的白名單允許哪些標(biāo)簽、哪些屬性遍歷整個樹移除或凈化所有不在白名單上的內(nèi)容最后返回安全的HTML。對于需要渲染用戶提供的富文本如博客評論、郵件模板的場景DOMPurify是行業(yè)標(biāo)準(zhǔn)選擇。import DOMPurify from dompurify; const cleanHTML DOMPurify.sanitize(dirtyHTML, { USE_PROFILES: { html: true } }); document.getElementById(output).innerHTML cleanHTML;利用現(xiàn)代框架的內(nèi)置保護像React、Vue、Angular這樣的現(xiàn)代前端框架默認(rèn)在渲染動態(tài)內(nèi)容時都會進(jìn)行轉(zhuǎn)義。例如在React中{userInput}會默認(rèn)進(jìn)行轉(zhuǎn)義只有明確使用dangerouslySetInnerHTML時才會產(chǎn)生風(fēng)險??蚣艿穆暶魇椒妒奖旧硪矞p少了直接操作DOM的機會。4.4 第四原則實施內(nèi)容安全策略CSPCSP是一個終極的深度防御措施。它不是一個修復(fù)具體漏洞的工具而是一個告訴瀏覽器“什么資源可以加載和執(zhí)行”的白名單機制。一個強化的CSP頭部可以顯著緩解XSS的影響甚至阻止其發(fā)生。一個針對DOM型XSS的嚴(yán)格CSP示例Content-Security-Policy: default-src self; script-src self unsafe-inline unsafe-eval; object-src none; base-uri self;讓我們拆解一下default-src ‘self’默認(rèn)只允許加載同源資源。script-src ‘self’只允許執(zhí)行來自同源的腳本。這直接阻止了通過eval()或內(nèi)聯(lián)事件處理器onclick”…”執(zhí)行的惡意腳本因為內(nèi)聯(lián)腳本和eval不符合‘self’它們不是外部文件。要允許內(nèi)聯(lián)腳本必須添加不安全的‘unsafe-inline’但這會削弱防護。最佳實踐是徹底不用內(nèi)聯(lián)腳本全部改為外部文件并使用nonce或hash來授權(quán)特定的內(nèi)聯(lián)腳本塊。object-src ‘none’禁止加載object,embed,applet等封堵其他可能的風(fēng)險點。base-uri ‘self’防止base標(biāo)簽被篡改從而防止相對路徑資源被劫持。實施CSP的挑戰(zhàn)在于它可能會“破壞”現(xiàn)有網(wǎng)站功能因為它太嚴(yán)格了。建議采用“報告-監(jiān)控-收緊”的流程先設(shè)置Content-Security-Policy-Report-Only頭部只報告違規(guī)而不阻止在控制臺和報告收集端點觀察哪些策略需要調(diào)整待所有違規(guī)都被解決后再切換到強制的CSP策略。5. 實戰(zhàn)審計與漏洞挖掘流程理論說再多不如動手挖一挖。這里分享一套我常用的DOM型XSS手動審計流程適合在代碼審計或黑盒測試時使用。5.1 靜態(tài)代碼審計白盒定位危險的“匯”在代碼庫中全局搜索關(guān)鍵詞如innerHTML、outerHTML、document.write、eval、setTimeout/Interval字符串參數(shù)、location.href賦值、Function構(gòu)造函數(shù)等?;厮輸?shù)據(jù)流對于每一個找到的“匯”向上追蹤其參數(shù)來源。這個參數(shù)是硬編碼的字符串還是來自某個變量這個變量又是在哪里被賦值的一直追溯到最初的輸入點“源”如location.search、document.referrer、window.name、postMessage事件處理器等。檢查凈化邏輯在數(shù)據(jù)從“源”到“匯”的路徑上是否有任何過濾、驗證或編碼函數(shù)這些函數(shù)是否足夠健壯是否考慮了上下文是否存在被繞過的可能如只過濾一次、黑名單不完整審查第三方庫的使用檢查項目中使用的模板引擎、UI組件庫、富文本編輯器等是否以安全的方式調(diào)用。查看其官方文檔中關(guān)于XSS防護的部分并確認(rèn)配置正確。5.2 動態(tài)測試與模糊測試黑盒在沒有源碼的情況下可以通過瀏覽器開發(fā)者工具進(jìn)行測試。識別數(shù)據(jù)接收點在應(yīng)用中尋找任何將用戶輸入反映到頁面上的功能。不僅僅是表單還包括URL參數(shù)變化引起頁面內(nèi)容更新、拖拽上傳后的預(yù)覽、通過WebSocket或EventSource接收并顯示的消息等。使用瀏覽器控制臺探測“匯”在疑似存在漏洞的頁面打開控制臺嘗試覆蓋一些關(guān)鍵的“匯”函數(shù)以觀察數(shù)據(jù)流。// 攔截對innerHTML的賦值打印出賦值的內(nèi)容和調(diào)用棧 var originalInnerHTML Object.getOwnPropertyDescriptor(Element.prototype, innerHTML).set; Object.defineProperty(Element.prototype, innerHTML, { set: function(value) { console.trace(innerHTML set called with value:, value); return originalInnerHTML.call(this, value); } });然后操作頁面觸發(fā)動態(tài)內(nèi)容更新看看控制臺輸出能清晰看到哪些數(shù)據(jù)被賦給了innerHTML。構(gòu)造測試載荷使用一個包含各種無害“探針”的測試字符串如‘“img srcx onerrorconsole.log(1)將其輸入到所有可疑的輸入點觀察是否觸發(fā)了console.log(1)。如果觸發(fā)了說明存在HTML注入點。可以進(jìn)一步嘗試更復(fù)雜的載荷。測試URL參數(shù)和片段手動修改URL的?參數(shù)和#片段觀察頁面內(nèi)容或JavaScript行為的變化。特別關(guān)注那些根據(jù)URL動態(tài)生成內(nèi)容的單頁應(yīng)用SPA。檢查CSP通過瀏覽器開發(fā)者工具的“網(wǎng)絡(luò)”標(biāo)簽或“控制臺”標(biāo)簽查看響應(yīng)頭中的Content-Security-Policy評估其嚴(yán)格程度。一個寬松的CSP如允許unsafe-inline會大大增加風(fēng)險。5.3 常見問題排查速查表在開發(fā)和測試過程中如果遇到疑似漏洞或防御相關(guān)的問題可以對照下表快速排查現(xiàn)象或問題可能的原因排查步驟與解決方案用戶提交的富文本樣式丟失HTML過濾過嚴(yán)白名單太窄。檢查使用的凈化庫如DOMPurify的配置適當(dāng)擴展允許的標(biāo)簽和屬性列表但需謹(jǐn)慎評估安全影響。使用了textContent但頁面顯示HTML標(biāo)簽字符串需求本就是顯示富文本錯用了textContent。確認(rèn)需求。如需渲染富文本應(yīng)改用安全的凈化庫處理后再用innerHTML。CSP策略導(dǎo)致網(wǎng)站功能如內(nèi)聯(lián)腳本、第三方統(tǒng)計失效CSP策略過于嚴(yán)格未授權(quán)必要的資源。1. 檢查瀏覽器控制臺的CSP違規(guī)報告。2. 將內(nèi)聯(lián)腳本改為外部文件或為其添加正確的nonce。3. 在script-src中按需添加可信的第三方域名。輸入已編碼但輸出仍亂碼或執(zhí)行了腳本編碼上下文錯誤或多次編碼/解碼。1. 確認(rèn)數(shù)據(jù)最終插入的上下文HTML內(nèi)容、屬性、JS、URL。2. 確保在最后一步、最接近輸出點的地方進(jìn)行針對該上下文的編碼。3. 避免在數(shù)據(jù)流中間進(jìn)行不必要的解碼操作。漏洞修復(fù)后自動化測試用例失敗測試用例可能依賴了有漏洞的舊行為或修復(fù)引入了副作用。1. 審查失敗的測試用例看它是否在測試一個本應(yīng)被阻止的惡意輸入。2. 如果是更新測試用例以匹配新的安全行為。3. 確保修復(fù)沒有破壞合法的功能。6. 構(gòu)建前端安全開發(fā)文化技術(shù)手段再強也抵不過人的疏忽。讓安全成為開發(fā)流程中自然而然的一部分才是長治久安之道。將安全培訓(xùn)納入入職和日常不僅僅是前端團隊所有涉及Web開發(fā)的工程師都應(yīng)了解XSS的基本原理和危害。定期組織內(nèi)部分享分析外部公開的漏洞案例。在代碼倉庫中集成安全掃描使用SAST靜態(tài)應(yīng)用安全測試工具如SonarQube、CodeQL將其集成到CI/CD流水線中。讓每一次提交都自動進(jìn)行安全檢查將問題暴露在開發(fā)早期。建立安全編碼規(guī)范在團隊wiki或代碼規(guī)范文檔中明確列出禁止使用的API如innerHTML、eval規(guī)定必須使用的安全替代方案和庫如textContent、DOMPurify。在代碼評審時將這些規(guī)范作為硬性檢查點。實施漏洞獎勵計劃如果條件允許可以建立內(nèi)部的漏洞報告機制甚至對外的漏洞獎勵計劃鼓勵大家主動發(fā)現(xiàn)和上報安全問題。定期進(jìn)行滲透測試和審計不要只依賴自動化工具體系。定期邀請專業(yè)的安全團隊或讓內(nèi)部的安全小組進(jìn)行手動滲透測試他們往往能發(fā)現(xiàn)自動化工具無法識別的邏輯漏洞和復(fù)雜的繞過鏈。DOM型XSS的防御是一場持久戰(zhàn)因為它緊密伴隨著前端技術(shù)的演進(jìn)。新的API、新的框架特性、新的業(yè)務(wù)場景都可能引入新的“源”和“匯”。作為開發(fā)者我們能做的就是建立起以“不信任任何用戶輸入”和“根據(jù)輸出上下文進(jìn)行編碼”為核心的安全心智模型并配以完善的技術(shù)工具和流程規(guī)范。這樣當(dāng)面對千變?nèi)f化的攻擊手法時我們才能有一個穩(wěn)固的基石來應(yīng)對而不僅僅是疲于奔命地打補丁。