不安全的直接對象引用:你的 ASP.NET 應用數據是否安全?

介紹

作爲一個在X94的航空工程師,你的老闆要求你從2號樓的工程圖中檢索出一個特定的專利。不幸的是,進入大樓需要你出示你具有進入大樓的資格的證明,然後你迅速地以徽章的形式出示給了保安。到了十三樓,進入建築師工程師圖紙庫要求通過他們的生物鑑定系統來驗證你是你聲稱的那個人。最後在你的目的地,你提供給庫管理員一串對你毫無意義的字母數字代碼,但是在合適的人手上,它可以轉換成哪裏可以找的你需要的工程圖的真實索引。

在上面的比喻中,我們可以很容易地確定適當的安全措施來保護敏感數據的訪問。除了個人訪問所需驗證,一個附加的可能不是很明顯的安全措施就是以字母數字碼的形式混淆技術文檔身份,並間接映射到真實的文檔身份和庫中的位置。

形象地說,這個比喻是一流行的被稱爲“非安全的直接對象引用”的Web應用安全漏洞的解答,該漏洞在OWASP最關鍵漏洞Top10中排第四。但如果這就是答案的話, 你接下來自然會問“關於我Web應用的具體問題是什麼且該如何去解決?”

不安全的直接對象引用

我們對在我們網站上展示商品的想法都很熟悉。用戶通過發起請求來查看商品詳情,向他們的購物車裏添加商品,或進行類似的活動。你很有可能會利用商品的ID去標識用戶正在請求哪件商品的詳細信息,標識添加進他們購物車的商品等等。最重要的是,這個ID很有可能是存儲商品信息的數據庫表的主鍵。如果真是這樣,那麼我們就擁有了一個直接對象引用。在網頁上展示的某個商品(對象)被特定的ID標識,而這個ID是對數據庫中相同標識的直接引用。

“說的不錯,但那又如何?”是這樣,在簡單的商家對顧客場景下,上文所講的情況不是什麼問題。但假定這是一個金融類服務應用,比方說是你最常用的網上銀行,上面有你的各個活期、定期儲蓄賬戶和其他敏感數據,那將會怎樣呢?想象一下,你在你的賬戶頁面選擇查看 ID 爲 1344573490 的存款賬戶的詳細信息:

不安全的直接對象引用:你的 ASP.NET 應用數據是否安全?

作爲一個經過身份覈實的名爲Mary Wiggins的用戶,網站顯示了針對你存款賬戶的信息:

不安全的直接對象引用:你的 ASP.NET 應用數據是否安全?

我們可以直接看出這個支票戶頭就是我們擁有的賬戶,同時也能確認這是一個直接引用。但要是你決定把 accountNumber 參數從 1344573490 改爲 1344573491,那將會發生什麼呢?

不安全的直接對象引用:你的 ASP.NET 應用數據是否安全?

Erin Maley,誰是Erin Maley?那不是我們。我們作爲Mary Wiggins是已經明確被認證過的。我們所有所做的事情就是順序地增加賬戶號直到下一個可能的值,並且我們可以看到一個不是我們所持有的賬戶信息。在這個例子中,我們有一個直接關聯的賬戶,它可以被定義爲系統內任何地方被標識的賬戶號。更進一步說,我們演習了一個潛在的問題,曝光一個直接相關的賬戶是簡單的數據工程。

如果你自己覺得這不是直接引用惹的禍,而是身份驗證上出了差錯,那麼你只對了一半。我們討論不安全直接對象引用所造成的缺陷時,實際上看到了兩個問題。我發現下圖能夠更清楚的描述這個缺陷究竟是什麼:

不安全的直接對象引用:你的 ASP.NET 應用數據是否安全?

如果不安全的直接對象引用涉及以下兩方面……

  1. 泄露敏感數據

  2. 缺乏合理的訪問控制

……那麼我們對於彌補這個缺陷的看法是什麼,以及我們應該何時採取行動?接下來,我們首先解決影響最大範圍最廣的問題——合理的訪問控制。

多層級的訪問控制

就像文章開頭舉的例子,多層級的訪問控制是必須的。雖然我們有權進入大樓,但進入樓內某些區域需要特定的權限。當我們考慮在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的傳入請求(請求一個賬號的詳細信息):

不安全的直接對象引用:你的 ASP.NET 應用數據是否安全?

我們可以看出,對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 開發者來說是一個可以理解的概念。

這個概念經過高度的精煉已經成爲構建 REST 風格的網絡服務的支柱之一:超媒體作爲應用程序狀態或 HATEOAS 的引擎。用一句話來解釋 HATEOAS 是網絡服務提供對於資源發現操作的能力:它通過在 HTTP 響應中提供超媒體鏈接。這不是一篇關於定義 REST 風格網絡服務的論文,所以,如果 REST 和 HATEOAS 對你來說是陌生概念,你需要查看關於 REST 和關於 HATEOAS 的資料來對他們有一個瞭解。

因此,提供包含有作用域的間接引用參數的 URL 的想法與像 HATEOAS 這樣的概念或需要一直提供持久性 URL (具有較長生存時間的 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);
   }

   private static byte[] GetEncrptedByteArray(
       SymmetricAlgorithm algorithm, byte[] iv, byte[] valueToBeEncrypted)
   {
       var encryptor = algorithm.CreateEncryptor(Key, iv);
       return encryptor.TransformFinalBlock(
           valueToBeEncrypted, 0, valueToBeEncrypted.Length);
   }

   private static AesManaged GetAlgorithm()
   {
       var aesManaged = new AesManaged
       {
           KeySize = KeySize,
           Mode = CipherMode.CBC,
           Padding = PaddingMode.PKCS7
       };
       return aesManaged;
    }

    private static byte[] GetRandomValue()
    {
       var csprng = new RNGCryptoServiceProvider();
       var buffer = new Byte[16];

       //generate the random indirect value
       csprng.GetBytes(buffer);
       return buffer;
    }
}

 

在這裏,我們的API應該看起來像ScopedReferenceMap,只有在發生變化時纔會在內部運行,我們藉助了.NET 中具有128位祕鑰的AesManaged對稱加密庫和一個對初始向量(IV)高度加密的隨機值。你們中的一些人可能會意識到,怎樣才能做到在速度與強度之間的最優化呢?

  1. AesManaged在實例中要比FIPS快約170倍,相當於AesCryptoServiceProvider

  2. 128位長度需要執行算法的次數少於4次,這要比更大的256位長度要小

關鍵點之一是我們爲初始向量(IV)生成一個強加密的隨機值,這個隨機值應用到了所有的加密過程中。祕鑰同樣是個機密,爲了保密,我選擇將它留給你,讓你來找出你想怎樣使用祕鑰,好的一方面是我們不必與任何人分享祕鑰。最終,我們存儲帶有密碼的非機密的初始向量(間接引用),這樣我們就可以在一個請求中解密間接引用。

要絕對地清楚,這不是一個可替代的訪問控制。這隻能用或應該用在正確的訪問控制連接上。

現在,也還有一個沒有那麼複雜的方法。一種改進過的方法是包含了上述過程的加密認證(AE),但是這是一個基於哈希消息驗證碼的過程。認證加密也支持像填充、消息篡改等暴漏的安全***。此外,像 Stan Drapkin那樣的學着會告訴你對稱加密必須被認證加密

然而,這並不是一篇關於加密的文章。所有的出發點就是以最後一個選項來“照亮”其他的選項,目的是給那些不間接使用作用域的用戶會話,如.NET,提供一個敏感數據的模糊環境。

牢記這些

  1. 緩解和減少不安全的直接對象引用的唯一可靠的方法是具有適當的訪問控制,再多的混淆都不能阻止對數據的未授權訪問。

  2. 資料是非常重要的,惡意用戶會以對他們有利的方式來使用它,當你意識到的時候就太晚了。因此,當你認爲一項數據是個敏感數據時,你需要應用一定等級的混淆來進行技術上的限制,例如使用用戶會話。但是會有一個.NET會話開銷,所以要知道你應該怎樣利用它。

  3. 絕大多數應用並不需要混淆和創建間接引用,但是對於像金融等高度敏感的網站最好加上這層額外的安全層。

  4. 最後一點是:對特定數據值的混淆只是一個模糊的安全。它需要與其它安全措施同時使用,例如正確的訪問控制。從這方面來說,不應該單獨依賴它。

總結

不安全的直接對象引用主要涉及的內容是,通過合理的訪問控制來保護數據不被未經授權的訪問。其次,爲了防止像直接引用鍵值那樣的敏感數據遭到泄露,要了解如何以及何時該通過間接引用那些鍵值來添加一層混淆。最後,在決定要使用混淆技術時,要意識到利用間接引用映射來彌補漏洞的侷限性。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章