場景回顧
一個表單進行跨域提交的方式有很多,我們使用的采用隱藏iframe,在本域下放一個代理頁面,通過服務端配合完成一次完整的請求。
首先,部署proxy.html代理頁面。這個頁面處理服務端返回的數據,並執行接口的回調函數。接口請求成功后,返回的是:
<script>location.href='http://www.a.com/proxy.html?fun=callback&a=1&b=2&c=3';</script>
proxy頁面,解析服務端傳回的參數最后執行:
callback({ a:1, b:2, c:3 });
這樣就完成了一次,接口請求並且在請求成功后,執行回調。
其次,頁面上需要有一個隱藏的iframe,把請求發到這個頁面,然后接受返回值。
整體上是這么一個過程,實現了post請求的跨域提交。
proxy.html的代碼:
<!doctype html> <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body><script type="text/javascript"> (function(){ var queryStr = location.search.substring(1).split('&'),oneQueryStr,args = {},g = top,scope = top ,callback; for(var i in queryStr){ oneQueryStr = queryStr[i].split('='); if(!callback && oneQueryStr[0] == 'fun'){ callback = oneQueryStr[1]; }; if(oneQueryStr[0]&&oneQueryStr[1]){ args[oneQueryStr[0]] = (oneQueryStr[1]||'').replace(/[><'"{}]/g, ''); } } callback = callback.split('.'); if( callback[0] === 'document' || callback[0] === 'location' || callback[0] === 'alert'){ }else{ for(var i = 0,len= callback.length;i<len;i++){ if(i==0 && callback[0]=="parent"){ g = parent; scope = parent; }else if(i==0 && callback[0]=="top"){ g = top; scope = top; }else{ if(i<len-1){ scope = scope[callback[i]]; } g = g[callback[i]]; } } g.call(scope,args); } })(); </script> </body></html>
XSS漏洞
很顯然,既然頁面上執行了接口返回的數據。那么就必須對返回的數據進行過濾,這樣才能防止惡意的代碼進行攻擊。
看了上面proxy.html的代碼我們發現,已經處理了fun這個參數的值,不能為alert、document、location這些值。這是通過黑名單機制來進行判斷的,一旦命中就無法執行。但是一直存在這么一個問題:我們能防的只是我們所了解的XSS漏洞,如果我們不知道,就需要遇到問題,解決問題,這樣顯然很被動。
有這么一個攻擊案例,即便是我們已經過濾了alert、document這些window下的方法,但是還有我們不知道的方法無法防御,這就增大了我們的維護成本。在我們業務里,帶來的問題是:每次調整這個文件,整個公司的業務可能都會被牽扯進來,都要升級很被動。
上面的這個例子就是掉用了頁面上給window下擴展的$,這個變量對於前端的同學都不陌生。這個$能干的事情就更多了,簡直太可怕了。這個案例只是打印了你的cookie,他可以做很多很多的事情,后果可以預想...
解決方案
還是要過濾fun參數,這個『萬惡之源』,采用的方式的白名單+黑名單,雙層防護。首先使用白名單進行過濾,白名單設置的是業務所用的函數名,其他的都不信任;其次,白名單如果驗證通過后,在進行黑名單驗證。為什么還需要這么異步操作呢?主要的擔心有這么一個方法,白名單會通過,但是又是無用的方法,這樣就可以使用黑名單在此過濾。
后續跟進
我現在的這個處理方式不一定是最好的,也可能存在問題。在這里提出,有兩方面的考慮。其一:想要獲得更好的方案來解決這個問題;其二:攻擊和防守是一個持續的問題,需要一直跟進,逐漸找到合理的解決方案。如果有更好的方案,也希望不吝賜教!