ASP.NET MVC Tip #10 - Prevent URL Manipulation Attacks
In this tip, I explain how hackers can steal sensitive information from an ASP.NET MVC website by manipulating URLs. I also discuss how you can build unit tests to prevent this type of attack.
A hacker can use a URL Manipulation Attack to easily access other people’s data at a website. If you retrieve records by the record Id, and you do not check with each database request that the right person is making the request, then anyone can read anyone else’s database records.
One of the benefits of ASP.NET MVC is that the framework exposes intuitive URLs. Unfortunately, this benefit also can be dangerous. A hacker can manipulate a URL to steal data from an ASP.NET MVC website.
Let’s walk through a simple sample application that is open to a URL Manipulation Attack. Imagine that you are building a website for a hospital. Hospital patients can login to the website to view their medical histories. This application has four views.
When a patient first makes a request to the application, the patient gets the view in Figure 1. This view contains a link that the patient can click to see their medical records.
Figure 1 – Index.aspx
If the patient has not already logged in, the patient is redirected to the Login view in Figure 2. The patient must enter the right credentials to see their medical records (The credentials are stored in the Web.config file).
Figure 2 – Login.aspx
After authenticating, the patient sees the Summary view in Figure 3. This view displays a list of links to detailed medical records. The database records are retrieved based on the patient’s user name.
Figure 3 – Summary.aspx
Finally, if a patient clicks a medical record link, the patient sees the Details view in Figure 4. This view displayed a single record.
Figure 4 – Details.aspx
Here’s how a hacker can steal patient data with a URL Manipulation Attack. Notice the URL in Figure 4 that was used to retrieve the detailed data for Phil. The URL looks like this:
http://localhost:48583/MedicalHistory/Details/6
This URL is very intuitive. Requesting this URL enables you to get the record with a database Id of 6. Because this URL is so intuitive, you can easily modify with another record number like so:
http://localhost:48583/MedicalHistory/Details/4
After changing the URL, Phil can see Rob’s personal hospital record as seen in Figure 5. This is not good.
The controller that returns the Summary and Details views is contained in Listing 1. The controller was written in such a way that it opens the hospital website to URL Manipulation Attacks.
Listing 1 – MedicalHistoryController.cs
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using System.Web.Mvc;
6: using Tip10.Models;
7:
8: namespace Tip10.Controllers
9: {
10: public class MedicalHistoryController : Controller
11: {
12: private readonly MedicalHistoryDataContext _db;
13:
14: public MedicalHistoryController()
15: : this(new MedicalHistoryDataContext())
16: { }
17:
18: public MedicalHistoryController(MedicalHistoryDataContext dataContext)
19: {
20: _db = dataContext;
21: }
22:
23:
24: public ActionResult Summary()
25: {
26: // Authenticate Guard Clause
27: if (!User.Identity.IsAuthenticated)
28: {
29: return RedirectToAction("Login", "Home");
30: }
31:
32: // Show summary of medical history
33: var records = from r in _db.MedicalHistories
34: where r.PatientUserName == User.Identity.Name
35: orderby r.EntryDate
36: select new SummaryMedicalHistory {Id=r.Id, EntryDate=r.EntryDate, Subject=r.Subject};
37: return View(records);
38: }
39:
40: public ActionResult Details(int id)
41: {
42: // Authenticate Guard Clause
43: if (!User.Identity.IsAuthenticated)
44: {
45: return RedirectToAction("Login", "Home");
46: }
47:
48: // Show detailed medical record
49: var record = _db.MedicalHistories.SingleOrDefault(r => r.Id == id);
50:
51: return View(record);
52: }
53:
54:
55: }
56: }
The MedicalHistoryController exposes two actions named Summary and Details. Both actions retrieve data from the MedicalHistory database table.
The Summary action is not open to URL Manipulation Attacks. When the database records are retrieved, the records are checked against the current patient’s user name. The records are retrieved with the following LINQ to SQL query:
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};
The bad query happens in the Details action. When the Details action retrieves a particular database record, only the Id of the record is used:
var record = _db.MedicalHistories.SingleOrDefault(r => r.Id == id);
Because of the way that this query is written, a hacker simply can change the Id passed to the Details action and see any other patient’s medical records.
Here’s the right way to create the query:
var record = _db.MedicalHistories.SingleOrDefault(r => r.Id == id &&
r.PatientUserName == User.Identity.Name);
In this rewritten database query, only a record that matches both the supplied Id and the current patient’s user name is returned. This database query is hacker safe.
Creating Unit Tests for URL Manipulation Attacks
It is easy to make a mistake when building an ASP.NET MVC website and open yourself up to a URL Manipulation Attack. How do you prevent this kind of mistake? One way is to write unit tests.
Consider the unit test in Listing 2.
Listing 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
{
[TestClass]
public class MedicalHistoryControllerTest
{
const string testDBPath = @"C:\Users\swalther\Documents\Common Content\Blog\Tip10 Prevent Querystring Manipulation Attacks\CS\Tip10\Tip10Tests\App_Data\MedicalHistoryDB_Test.mdf";
/// <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()
{
// 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)
{
// 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);
}
}
}
This unit test enables you to verify whether the Details action is open to URL Manipulation Attacks. Here’s how the test works.
First, I create a DataContext that represents a test database. The test database contains medical records for two fictitious patients named Phil and Rob. The test database is a copy of the production database with fake medical data.
Next, I mock the ControllerContext. I need to mock the ControllerContext because I want to simulate calling the Details action as Phil. I want to test whether I can access Rob’s medical records when I am authenticated as the user Phil.
I mock the ControllerContext with a method named GetMockUserContext(). This method uses a Mock Object Framework named Moq. You can learn more about Moq by reading the following blog entry:
http://weblogs.asp.net/stephenwalther/archive/2008/06/11/tdd-introduction-to-moq.aspx
Next, one of Rob’s medical records is returned from the test database. The Id of one of Rob’s records is passed to the Details() action within the user context of Phil.
Finally, an assertion is made that the record returned from the Details action is null. IF the record is not null, then the test has failed and Rob’s records can be stolen by Phil.
Conclusion
Be careful of URL Manipulation Attacks. If you need to protect sensitive data -- such as medical records or credit card numbers – then you need to be extremely careful about these types of attacks. In this tip, I described one approach for making your website more secure. Take advantage of unit tests to test your controller actions for URL manipulation attacks.
If you want to experiment with the code described in this blog entry, then you can download the code by clicking the following link. You will need to change the value of the testDBPath constant in the MedicalHistoryControllerTest file to match the path of the test medical database on your machine.