做爲一個在X94的航空工程師,你的老闆要求你從2號樓的工程圖中檢索出一個特定的專利。不幸的是,進入大樓須要你出示你具備進入大樓的資格的證實,而後你迅速地以徽章的形式出示給了保安。到了十三樓,進入建築師工程師圖紙庫要求經過他們的生物鑑定系統來驗證你是你聲稱的那我的。最後在你的目的地,你提供給庫管理員一串對你毫無心義的字母數字代碼,可是在合適的人手上,它能夠轉換成哪裏能夠找的你須要的工程圖的真實索引。web
在上面的比喻中,咱們能夠很容易地肯定適當的安全措施來保護敏感數據的訪問。除了我的訪問所需驗證,一個附加的可能不是很明顯的安全措施就是以字母數字碼的形式混淆技術文檔身份,並間接映射到真實的文檔身份和庫中的位置。數據庫
形象地說,這個比喻是一流行的被稱爲「非安全的直接對象引用」的Web應用安全漏洞的解答,該漏洞在OWASP最關鍵漏洞Top10中排第四。但若是這就是答案的話, 你接下來天然會問「關於我Web應用的具體問題是什麼且該如何去解決?」api
咱們對在咱們網站上展現商品的想法都很熟悉。用戶經過發起請求來查看商品詳情,向他們的購物車裏添加商品,或進行相似的活動。你頗有可能會利用商品的ID去標識用戶正在請求哪件商品的詳細信息,標識添加進他們購物車的商品等等。最重要的是,這個ID頗有多是存儲商品信息的數據庫表的主鍵。若是真是這樣,那麼咱們就擁有了一個直接對象引用。在網頁上展現的某個商品(對象)被特定的ID標識,而這個ID是對數據庫中相同標識的直接引用。安全
「說的不錯,但那又如何?」是這樣,在簡單的商家對顧客場景下,上文所講的狀況不是什麼問題。但假定這是一個金融類服務應用,比方說是你最經常使用的網上銀行,上面有你的各個活期、按期儲蓄帳戶和其餘敏感數據,那將會怎樣呢?想象一下,你在你的帳戶頁面選擇查看 ID 爲 1344573490 的存款帳戶的詳細信息:服務器
做爲一個通過身份覈實的名爲Mary Wiggins的用戶,網站顯示了針對你存款帳戶的信息:cookie
咱們能夠直接看出這個支票戶頭就是咱們擁有的帳戶,同時也能確認這是一個直接引用。但要是你決定把 accountNumber 參數從 1344573490 改成 1344573491,那將會發生什麼呢?網絡
若是你本身以爲這不是直接引用惹的禍,而是身份驗證上出了差錯,那麼你只對了一半。咱們討論不安全直接對象引用所形成的缺陷時,實際上看到了兩個問題。我發現下圖可以更清楚的描述這個缺陷到底是什麼:session
若是不安全的直接對象引用涉及如下兩方面……app
泄露敏感數據框架
缺少合理的訪問控制
……那麼咱們對於彌補這個缺陷的見解是什麼,以及咱們應該什麼時候採起行動?接下來,咱們首先解決影響最大範圍最廣的問題——合理的訪問控制。
就像文章開頭舉的例子,多層級的訪問控制是必須的。雖然咱們有權進入大樓,但進入樓內某些區域須要特定的權限。當咱們考慮在Web應用中保護資源時,可使用這樣的準則來達到目的。
首先,當前合法用戶是否有權請求資源?在咱們對該用戶一無所知的狀況下,該如何肯定當前用戶能夠被容許發起這個請求?所以第一步咱們要作的是,在和用戶交互時,經過添加訪問控制來保護資源。
在ASP.NET中,用戶交互經過控制器動做(controller action)完成。咱們能夠在ASP.NET MVC控制器上使用[Authorize]特性(attribute)來確保用戶只有先通過系統覈實身份才能執行控制器上的動做,而匿名用戶將被拒絕。
[Authorize] public class AccountsController : Controller { [HttpGet] public ActionResult Details(long accountNumber) { //...
這樣就確保了API沒法被公開使用,根據你的ASP.NET配置,用戶會被重定向到登陸頁面(默認行爲)。[Authorize]特性經過額外的約束來匹配特定的用戶和角色:
[Authorize(Roles = "Admin, Manager")] public class AccountsController : Controller { //..
[Authorize]特性除了能夠被應用到控制器動做上外,還能進行更多粒度的控制。例如在控制器上放置身份驗證約束,同時在控制器的不一樣動做上使用基於角色的訪問控制。
在咱們的銀行帳戶例子中,只對用戶進行身份驗證是不夠的,由於咱們(只通過身份驗證的用戶)居然能訪問另外一個用戶的支票帳戶信息。對於像銀行帳戶例子中看到的這種濫用行爲,一般被稱做爲橫向權限提高,用戶能夠訪問其餘相同等級的用戶信息。然而,有權發起對某個資源的請求與擁有對實際資源的權限是徹底不一樣的概念。
所以,咱們必須採起的第二層也是最重要訪問控制就是,保證用戶被受權訪問資源。在基於角色的訪問控制的狀況下,這就跟確保用戶屬於合理的角色同樣容易。若是被請求的資源只須要某個提高的權限,你能夠利用以前演示的[Authorize]的Role屬性來搞定。
[Authorize(Roles = "Admin")] public class AccountsController : Controller { //..
可是更多的時候,你被要求在數據層面對用戶進行權限驗證,以保證其有權訪問所請求的資源。考慮到受許多不一樣因素的影響,解決方案多種多樣,就上文提到的查看銀行帳戶詳情的案例,咱們能夠驗證用戶是否爲其所請求帳戶的擁有者:
[Authorize] public class AccountsController : Controller { [HttpGet] public ActionResult Details(long accountNumber) { Account account = _accountRepository.Find(accountNumber); if (account.UserId != User.Identity.GetUserId()) { return new HttpUnauthorizedResult("User is not Authorized."); } //...
記得咱們已經在控制器級別使用了[Authorize]特性,因此不必在動做級別多此一舉。
須要重點注意的是,在上面的關於在ASP.NET中使用Forms Authentication引起的非受權結果的例子中將會強制一個302跳轉到登錄頁面,不管用戶是否已經的到受權。所以,你或許須要對處理這種行爲做出必要的改變,這取決於你的應用,你的需求和你用戶的指望。你的選擇或者你是否須要處理這種行爲很大程度上依賴於框架的風格,使用OWIN模塊,和你的應用的須要。
好處是減小了去肯定沒有用戶提權的次數,保證了合適的訪問權限控制。至少,咱們能夠增強對請求自己和請求對被請求資源的訪問的訪問控制。可是,如同我前面提到的若干種場合, 在咱們的應用增強防止數據泄露老是應該評估的一個安全步驟。什麼是我所說的「數據泄露」?咱們能夠經過研究其餘包含不安全的直接對象引用(如混淆)來回答這個問題。
混淆 就是故意隱藏意圖的行爲。在咱們這兒, 咱們可使用混淆手段來推斷安全性。 一我的們認同的簡單例子就是URL短鏈。雖然初衷並非爲了安全性, 像這樣的URL http://bit.ly/1Gg2Pnn 是從真實的URL從混淆過來的。 根據這個短鏈, Bit.ly可以將混淆的URL http://bit.ly/1Gg2Pnn 映射到真正的http://lockmedown.com/preventing-xss-in-asp-net-made-easy.
咱們看到在前面咱們只是增長了賬號的數值就可以嚴格訪問另外一個用戶的支票賬戶,由於沒有數據級訪問控制。但咱們能夠經過混淆帳號創建另外一防護屏障使惡意用戶失去直接駕馭系統的能力,這經過改變數值就行。
能夠實現不一樣級別的混淆,每一級別都能提供不一樣級別的安全性和平衡性.咱們將看到第一個選項是一種比較常見的,安全的但有些限制的選項,我喜歡稱之爲「視野」,該詞間接參考地圖。
引用映射與 Bit.ly 短網址並無什麼不一樣,你的服務器知道怎樣將一個公開的表面值映射到一個內部值來表明敏感數據。做用域表明咱們用於限制映射使用而放入的限制條件。這對理論研究已經足夠了,咱們來看一個例子:
咱們認爲一個帳號編號例如1344573490是一個敏感數據,咱們但願隱藏它並只提供可被確認的帳號持有者。爲了不暴露帳號編號,咱們能夠提供一個間接引用到帳號編號的公開表面值。服務器將會知道怎樣把這個間接引用映射回直接引用,這個直接引用指向咱們的帳號編號。服務器使用的映射存儲在一個 ASP.NET 用戶回話中,這就是做用域,關於做用域的更多內容,來看看這個實現:
public static class ScopedReferenceMap { private const int Buffer = 32; /// <summary> /// Extension method to retrieve a public facing indirect value /// </summary> /// <typeparam name="T"></typeparam> /// <param name="value"></param> /// <returns></returns> public static string GetIndirectReference<T>(this T value) { //Get a converter to convert value to string var converter = TypeDescriptor.GetConverter(typeof (T)); if (!converter.CanConvertTo(typeof (string))) { throw new ApplicationException("Can't convert value to string"); } var directReference = converter.ConvertToString(value); return CreateOrAddMapping(directReference); } /// <summary> /// Extension method to retrieve the direct value from the user session /// if it doesn't exists, the session has ended or this is possibly an attack /// </summary> /// <param name="indirectReference"></param> /// <returns></returns> public static string GetDirectReference(this string indirectReference) { var map = HttpContext.Current.Session["RefMap"]; if (map == null ) throw new ApplicationException("Can't retrieve direct reference map"); return ((Dictionary<string, string>) map)[indirectReference]; } private static string CreateOrAddMapping(string directReference) { var indirectReference = GetUrlSaveValue(); var map = (Dictionary<string, string>) HttpContext.Current.Session["RefMap"] ?? new Dictionary<string, string>(); //If we have it, return it. if (map.ContainsKey(directReference)) return map[directReference]; map.Add(directReference, indirectReference); map.Add(indirectReference, directReference); HttpContext.Current.Session["RefMap"] = map; return indirectReference; } private static string GetUrlSaveValue() { var csprng = new RNGCryptoServiceProvider(); var buffer = new Byte[Buffer]; //generate the random indirect value csprng.GetBytes(buffer); //base64 encode the random indirect value to a URL safe transmittable value return HttpServerUtility.UrlTokenEncode(buffer); } }
這裏,咱們建立了一個簡單的工具類 ScopedReferenceMap,能夠提供擴展的方法處理一個值例如咱們的銀行卡號1344573490處理成 Xvqw2JEm84w1qqLN1vE5XZUdc7BFqarB0,這就是所謂的間接引用。
最終,當一個間接引用值被請求時,咱們使用一個用戶會話來做爲保持請求中的間接引用和直接引用之間的映射的一種方法。用戶會話成爲間接引用的做用域,並且強制在每一個用戶映射上加上時間限制。只有通過驗證和指定的用戶會話才具備檢索的能力。
你能夠利用它在任何你須要的地方建立間接引用,例如:
AccountNumber = accountNumber.GetIndirectReference(); //create an indirect reference
如今,在一個使用以下URL的傳入請求(請求一個帳號的詳細信息):
咱們能夠看出,對accountNumber的間接引用映射經過與咱們的訪問控制合做從新獲得真實值:
[HttpGet] public ActionResult Details(string accountNumber) { //get direct reference var directRefstr = accountNumber.GetDirectReference(); var accountNum = Convert.ToInt64(directRefstr); Account account = _accountRepository.Find(accountNum); //Verify authorization if (account.UserId != User.Identity.GetUserId()) { return new HttpUnauthorizedResult("User is not Authorized."); } //…
在咱們對得到直接引用的嘗試中,若是ASP.NET用戶會話沒有得到一個映射,那就多是受到了攻擊。可是,若是映射存在,仍然獲得了直接引用,則多是值被篡改了。
正如我前面提到的,用戶會話建立了一個做用域,用戶和時間約束限制了映射回直接引用的能力。這些限制條件以其自身的形式提供額外的安全措施。可是,你或許在使用 ASP.NET 會話狀態時遇到問題,這多是因爲已知的安全弱點,你也可能會問怎樣才能讓這些限制條件與提供含狀態傳輸(Representational State Transfer)風格的引擎例如超媒體狀態應用引擎良好的合做共處?真是個好問題,讓咱們來檢查一些替代選項吧。
HATEOAS Gonna Hate
若是你思考過經過網絡服務進行的典型交互方式,這種在你的應用中經過發送一個 request 和接受一個包含額外超媒體連接(例如 URLs)的 response 來得到額外的資源的方式對 web 開發者來講是一個能夠理解的概念。
所以,提供包含有做用域的間接引用參數的 URL 的想法與像 HATEOAS 這樣的概念或須要一直提供持久性 URL (具備較長生存時間的 URL )之間是有很大困難的。若是咱們但願提供持久性 URL 的同時,包含間接引用值,那麼咱們就須要採用一種不一樣的安全方法,咱們應該怎麼作呢?
靜態間接引用映射
假設你有一個 B2B 網絡應用,它容許商家得到指定給他們的 VIP 商品的訂價。給客戶系統發送一個請求,返回一個包含連接到此客戶的 VIP 商品的附加超媒體連接的響應。當點擊 VIP 商品連接時,接收到的響應就包含他們指定商家的全部可用 VIP 商品的超媒體連接。
在咱們的例子中,咱們決定經過建立一個間接引用,對VIP商品URL中的VIP商品ID加以混淆,到時候咱們能很快地從新映射回商品的實際ID。
例子: https://AppCore.com/business/Acme/VIP/Products/99933
針對咱們的處境,加密是一個不錯的選擇,這使得咱們能更好的掌控將間接引用映射回實際商品ID的生命週期。
如同咱們在域引用例子中作的那樣,利用相同的API,來看看它將會成爲何樣子,而後咱們帶着關注和額外的選擇,再討論一下咱們作了什麼和爲何用這種方法:
public static class StaticReferenceMap { public const int KeySize = 128; //bits public const int IvSize = 16; //bytes public const int OutputByteSize = KeySize / 8; private static readonly byte[] Key; static StaticReferenceMap() { Key = //pull 128 bit key in } /// <summary> /// Generates an encrypted value using symmetric encryption. /// This is utilizing speed over strength due to the limit of security through obscurity /// </summary> /// <typeparam name="T">Primitive types only</typeparam> /// <param name="value">direct value to be encrypted</param> /// <returns>Encrypted value</returns> public static string GetIndirectReferenceMap<T>(this T value) { //Get a converter to convert value to string var converter = TypeDescriptor.GetConverter(typeof (T)); if (!converter.CanConvertTo(typeof (string))) { throw new ApplicationException("Can't convert value to string"); } //Convert value direct value to string var directReferenceStr = converter.ConvertToString(value); //encode using UT8 var directReferenceByteArray = Encoding.UTF8.GetBytes(directReferenceStr); //Encrypt and return URL safe Token string which is the indirect reference value var urlSafeToken = EncryptDirectReferenceValue<T>(directReferenceByteArray); return urlSafeToken; } /// <summary> /// Give a encrypted indirect value, will decrypt the value and /// return the direct reference value /// </summary> /// <param name="indirectReference">encrypted string</param> /// <returns>direct value</returns> public static string GetDirectReferenceMap(this string indirectReference) { var indirectReferenceByteArray = HttpServerUtility.UrlTokenDecode(indirectReference); return DecryptIndirectReferenceValue(indirectReferenceByteArray); } private static string EncryptDirectReferenceValue<T>(byte[] directReferenceByteArray) { //IV needs to be a 16 byte cryptographic stength random value var iv = GetRandomValue(); //We will store both the encrypted value and the IV used - IV is not a secret var indirectReferenceByteArray = new byte[OutputByteSize + IvSize]; using (SymmetricAlgorithm algorithm = GetAlgorithm()) { var encryptedByteArray = GetEncrptedByteArray(algorithm, iv, directReferenceByteArray); Buffer.BlockCopy( encryptedByteArray, 0, indirectReferenceByteArray, 0, OutputByteSize); Buffer.BlockCopy(iv, 0, indirectReferenceByteArray, OutputByteSize, IvSize); } return HttpServerUtility.UrlTokenEncode(indirectReferenceByteArray); } private static string DecryptIndirectReferenceValue( byte[] indirectReferenceByteArray) { byte[] decryptedByteArray; using (SymmetricAlgorithm algorithm = GetAlgorithm()) { var encryptedByteArray = new byte[OutputByteSize]; var iv = new byte[IvSize]; //separate off the actual encrypted value and the IV from the byte array Buffer.BlockCopy( indirectReferenceByteArray, 0, encryptedByteArray, 0, OutputByteSize); Buffer.BlockCopy( indirectReferenceByteArray, encryptedByteArray.Length, iv, 0, IvSize); //decrypt the byte array using the IV that was stored with the value decryptedByteArray = GetDecryptedByteArray(algorithm, iv, encryptedByteArray); } //decode the UTF8 encoded byte array return Encoding.UTF8.GetString(decryptedByteArray); } private static byte[] GetDecryptedByteArray( SymmetricAlgorithm algorithm, byte[] iv, byte[] valueToBeDecrypted) { var decryptor = algorithm.CreateDecryptor(Key, iv); return decryptor.TransformFinalBlock( valueToBeDecrypted, 0, valueToBeDecrypted.Length); }
在這裏,咱們的API應該看起來像ScopedReferenceMap,只有在發生變化時纔會在內部運行,咱們藉助了.NET 中具備128位祕鑰的AesManaged對稱加密庫和一個對初始向量(IV)高度加密的隨機值。
如今,也還有一個沒有那麼複雜的方法。一種改進過的方法是包含了上述過程的加密認證(AE),可是這是一個基於哈希消息驗證碼的過程。認證加密也支持像填充、消息篡改等暴漏的安全攻擊。此外,像 Stan Drapkin那樣的學着會告訴你對稱加密必須被認證加密。
不安全的直接對象引用主要涉及的內容是,經過合理的訪問控制來保護數據不被未經受權的訪問。其次,爲了防止像直接引用鍵值那樣的敏感數據遭到泄露,要了解如何以及什麼時候該經過間接引用那些鍵值來添加一層混淆。最後,在決定要使用混淆技術時,要意識到利用間接引用映射來彌補漏洞的侷限性。