介绍
作为一个在X94的航空工程师,你的老板要求你从2号楼的工程图中检索出一个特定的专利。不幸的是,进入大楼需要你出示你具有进入大楼的资格的证明,然后你迅速地以徽章的形式出示给了保安。到了十三楼,进入建筑师工程师图纸库要求通过他们的生物鉴定系统来验证你是你声称的那个人。最后在你的目的地,你提供给库管理员一串对你毫无意义的字母数字代码,但是在合适的人手上,它可以转换成哪里可以找的你需要的工程图的真实索引。
在上面的比喻中,我们可以很容易地确定适当的安全措施来保护敏感数据的访问。除了个人访问所需验证,一个附加的可能不是很明显的安全措施就是以字母数字码的形式混淆技术文档身份,并间接映射到真实的文档身份和库中的位置。
形象地说,这个比喻是一流行的被称为“非安全的直接对象引用”的Web应用安全漏洞的解答,该漏洞在OWASP最关键漏洞Top10中排第四。但如果这就是答案的话, 你接下来自然会问“关于我Web应用的具体问题是什么且该如何去解决?”
不安全的直接对象引用
我们对在我们网站上展示商品的想法都很熟悉。用户通过发起请求来查看商品详情,向他们的购物车里添加商品,或进行类似的活动。你很有可能会利用商品的ID去标识用户正在请求哪件商品的详细信息,标识添加进他们购物车的商品等等。最重要的是,这个ID很有可能是存储商品信息的数据库表的主键。如果真是这样,那么我们就拥有了一个直接对象引用。在网页上展示的某个商品(对象)被特定的ID标识,而这个ID是对数据库中相同标识的直接引用。
“说的不错,但那又如何?”是这样,在简单的商家对顾客场景下,上文所讲的情况不是什么问题。但假定这是一个金融类服务应用,比方说是你最常用的网上银行,上面有你的各个活期、定期储蓄账户和其他敏感数据,那将会怎样呢?想象一下,你在你的账户页面选择查看 ID 为 1344573490 的存款账户的详细信息:
作为一个经过身份核实的名为Mary Wiggins的用户,网站显示了针对你存款账户的信息:
我们可以直接看出这个支票户头就是我们拥有的账户,同时也能确认这是一个直接引用。但要是你决定把 accountNumber 参数从 1344573490 改为 1344573491,那将会发生什么呢?
Erin Maley,谁是Erin Maley?那不是我们。我们作为Mary Wiggins是已经明确被认证过的。我们所有所做的事情就是顺序地增加账户号直到下一个可能的值,并且我们可以看到一个不是我们所持有的账户信息。在这个例子中,我们有一个直接关联的账户,它可以被定义为系统内任何地方被标识的账户号。更进一步说,我们演习了一个潜在的问题,曝光一个直接相关的账户是简单的数据工程。
如果你自己觉得这不是直接引用惹的祸,而是身份验证上出了差错,那么你只对了一半。我们讨论不安全直接对象引用所造成的缺陷时,实际上看到了两个问题。我发现下图能够更清楚的描述这个缺陷究竟是什么:
如果不安全的直接对象引用涉及以下两方面……
- 泄露敏感数据
- 缺乏合理的访问控制
……那么我们对于弥补这个缺陷的看法是什么,以及我们应该何时采取行动?接下来,我们首先解决影响最大范围最广的问题——合理的访问控制。
多层级的访问控制
就像文章开头举的例子,多层级的访问控制是必须的。虽然我们有权进入大楼,但进入楼内某些区域需要特定的权限。当我们考虑在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 开发者来说是一个可以理解的概念。
这个概念经过高度的精炼已经成为构建 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)高度加密的随机值。你们中的一些人可能会意识到,怎样才能做到在速度与强度之间的最优化呢?
- AesManaged在实例中要比FIPS快约170倍,相当于AesCryptoServiceProvider
- 128位长度需要执行算法的次数少于4次,这要比更大的256位长度要小
关键点之一是我们为初始向量(IV)生成一个强加密的随机值,这个随机值应用到了所有的加密过程中。秘钥同样是个机密,为了保密,我选择将它留给你,让你来找出你想怎样使用秘钥,好的一方面是我们不必与任何人分享秘钥。最终,我们存储带有密码的非机密的初始向量(间接引用),这样我们就可以在一个请求中解密间接引用。
要绝对地清楚,这不是一个可替代的访问控制。这只能用或应该用在正确的访问控制连接上。
现在,也还有一个没有那么复杂的方法。一种改进过的方法是包含了上述过程的加密认证(AE),但是这是一个基于哈希消息验证码的过程。认证加密也支持像填充、消息篡改等暴漏的安全攻击。此外,像 Stan Drapkin那样的学着会告诉你对称加密必须被认证加密。
然而,这并不是一篇关于加密的文章。所有的出发点就是以最后一个选项来“照亮”其他的选项,目的是给那些不间接使用作用域的用户会话,如.NET,提供一个敏感数据的模糊环境。
牢记这些
- 缓解和减少不安全的直接对象引用的唯一可靠的方法是具有适当的访问控制,再多的混淆都不能阻止对数据的未授权访问。
- 资料是非常重要的,恶意用户会以对他们有利的方式来使用它,当你意识到的时候就太晚了。因此,当你认为一项数据是个敏感数据时,你需要应用一定等级的混淆来进行技术上的限制,例如使用用户会话。但是会有一个.NET会话开销,所以要知道你应该怎样利用它。
- 绝大多数应用并不需要混淆和创建间接引用,但是对于像金融等高度敏感的网站最好加上这层额外的安全层。
- 最后一点是:对特定数据值的混淆只是一个模糊的安全。它需要与其它安全措施同时使用,例如正确的访问控制。从这方面来说,不应该单独依赖它。
总结
不安全的直接对象引用主要涉及的内容是,通过合理的访问控制来保护数据不被未经授权的访问。其次,为了防止像直接引用键值那样的敏感数据遭到泄露,要了解如何以及何时该通过间接引用那些键值来添加一层混淆。最后,在决定要使用混淆技术时,要意识到利用间接引用映射来弥补漏洞的局限性。