原文地址:http://weblogs.asp.net/stephenwalther/archive/2008/06/26/prevent-url-manipulation-attacks.aspx
摘要:在这个Tip中,Stephen Walther介绍了黑客如何通过操作URL从ASP.NET MVC网站中窃取敏感信息。Stephen Walther还探讨了如何构建单元测试来防止这类攻击。
在一个网站上,黑客可以通过URL操作攻击来简单地访问其他用户的数据。如果你通过记录的ID来获取数据记录,而又没有针对每个数据库请求检查是否是由正确的用户发起的请求,则任何人都可以读取其他用户的数据库记录。
ASP.NET MVC框架的一个优势在于,它可以暴露出直观的URL。不幸的是,这个优势也可能是危险的。黑客可以通过操作URL来从一个ASP.NET MVC网站中窃取数据。
我们来看一个简单的示例程序,它将面临URL操作攻击。假设你正在为医院创建网站。医院的病人可以登录网站来查看他们的病历。这个应用程序有四个视图。
当病人第一次向该应用程序发起请求时,他会看到图1所示的视图。该视图包含一个链接,病人单击这个链接可以看到他的病历。
图1 - Index.aspx
![](http://weblogs.asp.net/blogs/stephenwalther/WindowsLiveWriter/PreventURLManipulationAttacks_E6D1/clip_image002_thumb.jpg)
如果病人尚未登录,他将被重定向到如图2所示的Login视图。病人必须输入正确的凭证才能查看他的病历记录(凭证存放在Web.config中)。
图2 - Login.aspx
![](http://weblogs.asp.net/blogs/stephenwalther/WindowsLiveWriter/PreventURLManipulationAttacks_E6D1/clip_image004_thumb.jpg)
通过验证后,病人会看到图3所示的Summary视图。该视图显示了一个列表,给出了一组指向详细病历记录的链接。数据记录的获取是根据病人的用户名进行的。
图3 - Summary.aspx
![](http://weblogs.asp.net/blogs/stephenwalther/WindowsLiveWriter/PreventURLManipulationAttacks_E6D1/clip_image006_thumb.jpg)
最后,如果病人单击了一个病历记录链接,他就会看到如图4所示的Details视图。该视图显示了一条单独的记录。
图4 - Details.aspx
![](http://weblogs.asp.net/blogs/stephenwalther/WindowsLiveWriter/PreventURLManipulationAttacks_E6D1/clip_image008_thumb.jpg)
这里就是黑客能够通过URL操作攻击来窃取病人数据的地方了。注意图4中用于为Phil(病人名)获取详细数据的URL。该URL看上去是这样的:
http://localhost:48583/MedicalHistory/Details/6
该URL非常直观。请求这个URL可以获取数据库中Id是6的数据。由于这个URL是如此的直观,你可以很容易将其修改为另外一个编号——
http://localhost:48583/MedicalHistory/Details/4
修改了URL之后,Phil可以看到Rob的私人病历记录,如图5所示。这恐怕不好吧。
图5 - Phil可以看到Rob的私人记录
![](http://weblogs.asp.net/blogs/stephenwalther/WindowsLiveWriter/PreventURLManipulationAttacks_E6D1/clip_image010_thumb.jpg)
清单1列出了用于返回Summary和Details视图的控制器。这样编写控制器导致该医院网站为URL操作攻击敞开了大门。
清单1 - MedicalHistoryCotroller.cs
清单1 - MedicalHistoryCotroller.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Tip10.Models;
namespace Tip10.Controllers
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
{
public class MedicalHistoryController : Controller
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
private readonly MedicalHistoryDataContext _db;
public MedicalHistoryController()
: this(new MedicalHistoryDataContext())
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{ }
public MedicalHistoryController(MedicalHistoryDataContext dataContext)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
_db = dataContext;
}
public ActionResult Summary()
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
// Authenticate Guard Clause
if (!User.Identity.IsAuthenticated)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
return RedirectToAction("Login", "Home");
}
// Show summary of medical history
var records = from r in _db.MedicalHistories
where r.PatientUserName == User.Identity.Name
orderby r.EntryDate
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
select new SummaryMedicalHistory
{Id=r.Id, EntryDate=r.EntryDate, Subject=r.Subject};
return View(records);
}
public ActionResult Details(int id)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
// Authenticate Guard Clause
if (!User.Identity.IsAuthenticated)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
return RedirectToAction("Login", "Home");
}
// Show detailed medical record
var record = _db.MedicalHistories.SingleOrDefault(r => r.Id == id);
return View(record);
}
}
}
MedicalHistoryController暴露了两个操作,名字分别是Summary和Details。两个操作都从MedicalHistory数据表中获取数据。
Summary操作并没有为URL操作攻击敞开大门。在获取数据库记录时,记录是针对当前病人的用户名检查过的。记录是通过下面的LINQ to SQL查询获取的:
var records
=
from r
in
_db.MedicalHistories
where
r.PatientUserName
==
User.Identity.Name
orderby r.EntryDate
select
new
SummaryMedicalHistory {Id
=
r.Id, EntryDate
=
r.EntryDate, Subject
=
r.Subject};
不好的查询出现在Details操作中。当Details操作获取一条特定的数据库记录时,只使用了记录的Id:
var record
=
_db.MedicalHistories.SingleOrDefault(r
=>
r.Id
==
id);
由于这样编写了查询,黑客只需简单地修改传给Details操作的Id就能看到其他病人的病历记录。
下面是编写查询的正确方法:
var record
=
_db.MedicalHistories.SingleOrDefault(r
=>
r.Id
==
id
&&
r.PatientUserName
==
User.Identity.Name);
在这个修改过的查询中,只有同时匹配指定Id和当前病人用户名的记录会返回。这个数据库查询是安全的。
针对URL操作攻击创建单元测试
在构建ASP.NET MVC网站时很容易出现错误,使得你自己被暴露在URL操作攻击下。如何防止这种错误?编写单元测试是一种办法。
考虑清单2中的单元测试。
清单2 - MedicalHistoryControllerTest.cs
清单2 - MedicalHistoryControllerTest.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Security.Principal;
using Tip10.Controllers;
using Tip10.Models;
using Moq;
namespace Tip10Tests.Controllers
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
{
[TestClass]
public class MedicalHistoryControllerTest
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
const string testDBPath = @"C:\Users\swalther\Documents\Common Content\Blog\Tip10 Prevent Querystring Manipulation Attacks\CS\Tip10\Tip10Tests\App_Data\MedicalHistoryDB_Test.mdf";
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//// <summary>
/// Tests that Phil cannot read Rob's database records
/// Mocks ControllerContext to mock Phil's identity
/// and attempts to grab one of Rob's records. The
/// result had better be null or their is a querystring
/// manipulation violation.
/// </summary>
[TestMethod]
public void DetailsCheckForURLAttack()
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
// Arrange
var testDataContext = new MedicalHistoryDataContext(testDBPath);
var controller = new MedicalHistoryController(testDataContext);
controller.ControllerContext = GetMockUserContext("Phil", true);
// Act
var robRecord = testDataContext.MedicalHistories
.FirstOrDefault(h => h.PatientUserName == "Rob");
var result = controller.Details(robRecord.Id) as ViewResult;
var medicalHistory = (MedicalHistory)result.ViewData.Model;
// Assert
Assert.IsNull(medicalHistory, "Phil can read Rob's medical records!");
}
private static ControllerContext GetMockUserContext(string userName, bool isAuthenticated)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
// Mock Identity
var mockIdentity = new Mock<IIdentity>();
mockIdentity.ExpectGet(i => i.Name).Returns(userName);
mockIdentity.ExpectGet(i => i.IsAuthenticated).Returns(isAuthenticated);
// Mock Principal
var mockPrincipal = new Mock<IPrincipal>();
mockPrincipal.ExpectGet(p => p.Identity).Returns(mockIdentity.Object);
// Mock HttpContext
var mockHttpContext = new Mock<HttpContextBase>();
mockHttpContext.ExpectGet(c => c.User).Returns(mockPrincipal.Object);
return new ControllerContext(mockHttpContext.Object, new RouteData(), new Mock<IController>().Object);
}
}
}
该单元测试允许你检查Details操作是否为URL操作攻击敞开了大门。这里是测试的工作流程。
首先,我创建了一个DataContext来呈现测试数据库。测试数据库中包含了两位假想病人(Phil和Rob)的病历记录。测试数据库是产品数据库的副本,但其中包含的是假数据。
接下来,我mock了ControllerContext。我必须mock一个ControllerContext,因为我想在调用Details操作时假装成是Phil。我希望测试当我被验证为是用户Phil时,是否能访问Rob的病历记录。
我是用了一个名为GetMockUserContext()的方法来mock这个ControllerContext。该法官法使用了名为Moq的Mock Object Framework。有关Moq的更多信息,请阅读下面这篇博客文章:
http://weblogs.asp.net/stephenwalther/archive/2008/06/11/tdd-introduction-to-moq.aspx
接下来,从测试数据库返回了一条Rob的病历记录。Rob的病历记录的Id是由Phil用户传递给Details操作的。
最后,会根据Details操作返回的记录是否为null产生一个断言。如果记录不是null,则测试会失败,Rob的记录可能会被Phil偷走。
小结
要小心URL操作攻击。如果你需要保护敏感数据——如病历记录和信用卡号——你需要特别小心这类攻击。在这个Tip中,我介绍了一种让你的网站更加安全的途径。请利用单元测试来针对URL操作攻击测试你的控制器操作。
如果你想试验本文中的代码,请单击下面的链接下载源代码。你需要修改MedicalHistoryControllTest文件中testDBPath的值,使其对应你机器上的测试用病历数据库。
此处下载源代码:http://weblogs.asp.net/blogs/stephenwalther/Downloads/Tip10/Tip10.zip。
-----
广告:.NET正则表达式库,http://regex-lib.net。
转载于:https://www.cnblogs.com/AndersLiu/archive/2008/07/17/prevent-url-manipulation-attacks.html