Problem
How to prevent insecure direct object reference in ASP.NET Core.
Solution
Create an empty project and update the Startup class.
- public void ConfigureServices(
- IServiceCollection services)
- {
- services.AddMvc();
- services.AddDataProtection();
- }
-
- public void Configure(
- IApplicationBuilder app,
- IHostingEnvironment env)
- {
- app.UseMvcWithDefaultRoute();
- }
Create a model.
- public class Movie
- {
- public int Id { get; set; }
- public string Title { get; set; }
- public int ReleaseYear { get; set; }
- public string Summary { get; set; }
- }
Create a controller and inject IDataProtectionProvider as dependency.
- [Route("movies")]
- public class MoviesController : Controller
- {
- private readonly IDataProtector protector;
-
- public MoviesController(IDataProtectionProvider provider)
- {
- this.protector = provider.CreateProtector("protect_my_query_string");
- }
- ...
Add a method that retrieves the data and then, encrypt the object reference (Id property here).
- [HttpGet]
- public IActionResult Get()
- {
- var model = GetMovies();
-
- var outputModel = model.Select(item => new
- {
- Id = this.protector.Protect(item.Id.ToString()),
- item.Title,
- item.ReleaseYear,
- item.Summary
- });
-
- return Ok(outputModel);
- }
Add a method that receives the encrypted object reference and then decrypts it.
- [HttpGet("{id}")]
- public IActionResult Get(string id)
- {
- var orignalId = int.Parse(this.protector.Unprotect(id));
-
- var model = GetMovies();
-
- var outputModel = model.Where(item => item.Id == orignalId);
-
- return Ok(outputModel);
- }
Running the sample (browsing to /movies) with show encrypted references.
Discussion
OWASP 2013 classifies Insecure Direct Object Reference as one of the top 10 risks and is present if the object references (e.g. primary key of a database record) can be manipulated for malicious attacks.
One possible method to prevent is shown in the example above, i.e. by encrypting the internal references we can hide the internal details of our database/application structure. We can encrypt in various ways, here I am using the new Data Protection API in ASP.NET Core.
Data Protection API
There are two key abstractions we utilize to encrypt the data, IDataProtectionProvider and IDataProtector. We use the provider to create a protector by calling its CreateProtector() method. This method takes in a string key (known as Purpose String). Once we have a protector, we can use the Protect() method to encrypt and the Unprotect() method to decrypt the data.
Purpose Strings
It’s a key that ensures isolation between different protectors (cryptographic consumers) i.e. data encrypted by protector A cannot be read by a protector B, as long as they use different purpose strings.
Exception
Trying to decrypt the data that has been modified will throw CryptographicException,
Limited Lifetime
You could also encrypt data that can be decrypted for only a limited period of time (e.g. creating tokens for password reset link). In order to achieve this, we use ITimeLimitedDataProtector and specify a time period when protecting the data,
- private readonly ITimeLimitedDataProtector protector;
-
- public MoviesController(IDataProtectionProvider provider)
- {
- this.protector = provider.CreateProtector("protect_my_query_string")
- .ToTimeLimitedDataProtector();
- }
-
- [HttpGet]
- public IActionResult Get()
- {
- var model = GetMovies();
-
- var outputModel = model.Select(item => new
- {
- Id = this.protector.Protect(item.Id.ToString(),
- TimeSpan.FromSeconds(10)),
- item.Title,
- item.ReleaseYear,
- item.Summary
- });
-
- return Ok(outputModel);
- }
Trying to decrypt the data after it’s expiry will throw CryptographicException,
Tip - Action Filter
You could create an action filter that decrypts the incoming encrypted reference, this can be reused across your application.
- [HttpGet("{id}")]
- [DecryptReference]
- public IActionResult Get(int id)
- {
- var model = GetMovies();
-
- var outputModel = model.Where(item => item.Id == id);
-
- return Ok(outputModel);
- }
A simple filter (note I am using typed filter as it has dependency injection).
- public class DecryptReferenceFilter : IActionFilter
- {
- private readonly IDataProtector protector;
-
- public DecryptReferenceFilter(IDataProtectionProvider provider)
- {
- this.protector = provider.CreateProtector("protect_my_query_string");
- }
-
- public void OnActionExecuting(ActionExecutingContext context)
- {
- object param = context.RouteData.Values["id"].ToString();
- var id = int.Parse(this.protector.Unprotect(param.ToString()));
- context.ActionArguments["id"] = id;
- }
-
- public void OnActionExecuted(ActionExecutedContext context)
- {
-
- }
- }
-
- public class DecryptReferenceAttribute : TypeFilterAttribute
- {
- public DecryptReferenceAttribute() :
- base(typeof(DecryptReferenceFilter))
- { }
- }
Configuration
You could configure the data protection API when configuring its service.
- public void ConfigureServices(
- IServiceCollection services)
- {
- services.AddMvc();
- services.AddDataProtection()
- .SetApplicationName("Fiver.Security")
- .PersistKeysToFileSystem(new
- DirectoryInfo(@"C:\MyKeys"))
- .SetDefaultKeyLifetime(TimeSpan.FromDays(7))
- .UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
- {
- EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
- ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
- });
- }
Here I am configuring,
- Application name e.g. in order to have multiple projects in my application (e.g. APIs) decrypt the data.
- Location where keys are stored
- A lifetime of keys. The default is 90 days and minimum being 7 days.
- Cryptographic algorithm: just resetting to default explicitly for demonstration purposes.
Source Code