不安全的直接对象引用:你的 ASP.NET 应用数据是否安全?

介绍

做为一个在X94的航空工程师,你的老板要求你从2号楼的工程图中检索出一个特定的专利。不幸的是,进入大楼须要你出示你具备进入大楼的资格的证实,而后你迅速地以徽章的形式出示给了保安。到了十三楼,进入建筑师工程师图纸库要求经过他们的生物鉴定系统来验证你是你声称的那我的。最后在你的目的地,你提供给库管理员一串对你毫无心义的字母数字代码,可是在合适的人手上,它能够转换成哪里能够找的你须要的工程图的真实索引。web

在上面的比喻中,咱们能够很容易地肯定适当的安全措施来保护敏感数据的访问。除了我的访问所需验证,一个附加的可能不是很明显的安全措施就是以字母数字码的形式混淆技术文档身份,并间接映射到真实的文档身份和库中的位置。数据库

形象地说,这个比喻是一流行的被称为“非安全的直接对象引用”的Web应用安全漏洞的解答,该漏洞在OWASP最关键漏洞Top10中排第四。但若是这就是答案的话, 你接下来天然会问“关于我Web应用的具体问题是什么且该如何去解决?”api

不安全的直接对象引用

咱们对在咱们网站上展现商品的想法都很熟悉。用户经过发起请求来查看商品详情,向他们的购物车里添加商品,或进行相似的活动。你颇有可能会利用商品的ID去标识用户正在请求哪件商品的详细信息,标识添加进他们购物车的商品等等。最重要的是,这个ID颇有多是存储商品信息的数据库表的主键。若是真是这样,那么咱们就拥有了一个直接对象引用。在网页上展现的某个商品(对象)被特定的ID标识,而这个ID是对数据库中相同标识的直接引用。安全

“说的不错,但那又如何?”是这样,在简单的商家对顾客场景下,上文所讲的状况不是什么问题。但假定这是一个金融类服务应用,比方说是你最经常使用的网上银行,上面有你的各个活期、按期储蓄帐户和其余敏感数据,那将会怎样呢?想象一下,你在你的帐户页面选择查看 ID 为 1344573490 的存款帐户的详细信息:服务器

不安全的直接对象引用:你的 ASP.NET 应用数据是否安全?

做为一个通过身份核实的名为Mary Wiggins的用户,网站显示了针对你存款帐户的信息:cookie

不安全的直接对象引用:你的 ASP.NET 应用数据是否安全?

咱们能够直接看出这个支票户头就是咱们拥有的帐户,同时也能确认这是一个直接引用。但要是你决定把 accountNumber 参数从 1344573490 改成 1344573491,那将会发生什么呢?网络

不安全的直接对象引用:你的 ASP.NET 应用数据是否安全?

若是你本身以为这不是直接引用惹的祸,而是身份验证上出了差错,那么你只对了一半。咱们讨论不安全直接对象引用所形成的缺陷时,实际上看到了两个问题。我发现下图可以更清楚的描述这个缺陷到底是什么:session

不安全的直接对象引用:你的 ASP.NET 应用数据是否安全?

若是不安全的直接对象引用涉及如下两方面……app

  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 开发者来讲是一个能够理解的概念。

所以,提供包含有做用域的间接引用参数的 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那样的学着会告诉你对称加密必须被认证加密

总结

不安全的直接对象引用主要涉及的内容是,经过合理的访问控制来保护数据不被未经受权的访问。其次,为了防止像直接引用键值那样的敏感数据遭到泄露,要了解如何以及什么时候该经过间接引用那些键值来添加一层混淆。最后,在决定要使用混淆技术时,要意识到利用间接引用映射来弥补漏洞的局限性。

相关文章
相关标签/搜索