不安全的直接物件引用:你的 ASP.NET 應用資料是否安全?

oschina發表於2015-04-15

介紹

作為一個在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模組,和你的應用的需要。

好處是減少了去確定沒有使用者提權的次數,保證了合適的訪問許可權控制。至少,我們可以加強對請求本身和請求對被請求資源的訪問的訪問控制。但是,如同我前面提到的若干種場合, 在我們的應用加強防止資料洩露總是應該評估的一個安全步驟。什麼是我所說的“資料洩露”?我們可以通過研究其他包含不安全的直接物件引用(如混淆)來回答這個問題。

如果你對此感興趣,請在Twitter上follow我Follow @maxrmccarty

混淆

混淆 就是故意隱藏意圖的行為。在我們這兒, 我們可以使用混淆手段來推斷安全性。 一個人們認同的簡單例子就是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&apos;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&apos;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&apos;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.");
        }

        //&hellip;

在我們對獲得直接引用的嘗試中,如果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&apos;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. 最後一點是:對特定資料值的混淆只是一個模糊的安全。它需要與其它安全措施同時使用,例如正確的訪問控制。從這方面來說,不應該單獨依賴它。

總結

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

相關文章