IDOR prevention in ASP.NET Core

Imagine you start a promo campaign to say thank you to your most loyal users. You send them links for activating special conditions for using your service. Soon you notice that instead of getting a discount for one month, they grant themselves almost lifetime access. You close the campaign, making angry not-so-fast but still loyal […]

Imagine you start a promo campaign to say thank you to your most loyal users. You send them links for activating special conditions for using your service. Soon you notice that instead of getting a discount for one month, they grant themselves almost lifetime access.

You close the campaign, making angry not-so-fast but still loyal users, and start looking for a source of the issue. It turns out that the problem was with the promo codes.

Every user receives a link that looks something like this: “https://service.com/promo?code=2”. Smart (but still loyal) users try their luck with code #3, then #4, and at some time they get to their lucky number with a lot of subsequent ones. Pretty obvious, but still a problem.

Or it may be another issue with users getting access to resources they are not supposed to reach. For example, the photo of your favorite puppy can be found by this link “https://photos.com/1001”. But you can’t deny that it’s so tempting to try what is in “https://photos.com/1002” and so on. 

There are a couple of ways to deal with this issue:

  • Basic authorization with access control policies, defining what resource each user can get (the best solution for the second scenario with retrieving other users’ photos)
  • Input validation (checking what exactly the user wants to get)

Of course, all these scenarios may sound trivial. But websites nowadays still suffer from different vulnerabilities, and there is one more solution to deal with them – insecure direct object reference prevention.

The main idea is not to expose the identifiers of the resources for the end-user. There will be “incomprehensible garbage” that only your backend can deal with, getting it from requests, routes, or query parameters. For example, “https://photos.com/gfdkj4t34-g0gd _V” instead of “https://photos.com/1001”. We’ll implement this in a basic .NET Web API application that will be able to encrypt all outcoming IDs and decrypt incoming ones.

One class to rule them all

First, we need one class that will represent this encrypted value. 

It’s important since we don’t want to do encryption manually every time. We could also make middleware that would encrypt all values of some specific type, but that is a very inflexible solution. For example, if IDs in your system are integers, there is no way for returning all of them as encrypted.

Probably, it’ll be easier to represent IDs as Guid (UUID) values, but imagine that at some point, you’d need to return some decrypted value. It creates difficulties.

Instead of adding a separate class, it’s also possible to make an attribute, but that’s not convenient. So we’ll go with a separate class.

Thinking ahead, we can make this class generic to encrypt data of any type: from string and integers to Guids. But if you’re 100% sure that it’ll be used just for your IDs, then make it strongly-typed.

This class won’t store any complex logic. We need it only for storing data about the identifier. It’s going to have one property with the id itself that we can access directly and a constructor for creating new instances.

public class Id<T>
{
    public T Value { get; }

    public Id(T value)
    {
        Value = value;
    }
}

From now on we can use this type for our identifiers. For example,

public class Entity
{
    public Id<Guid> Id { get; set; }
    public string name { get; set; }
}

If you do decide to make this class strongly-typed and use it only for the IDs (that are of Guid type), then it will be like this:

public class Id
{
    public Guid Value { get; }

    public Id(Guid value)
    {
        Value = value;
    }
}

Encryptor

As for the encryption, we can implement it ourselves or use existing libraries. Since the main idea behind this material is to show a practical implementation of indirect object reference prevention, there is no sense in reinventing the wheel. I’m using NETCore.Encrypt (https://www.nuget.org/packages/NETCore.Encrypt).

Though there may be situations when you can consider creating your solution. For example, if you go with Guids, you can significantly reduce the length of your final value. Since AES encryption works with blocks of 128 bits and one Guid is exactly 128 bits, you can get rid of paddings, which would result in a much shorter encrypted value (though no padding may reduce security also). But it’s just a point for consideration.

So, our encryption class can be as simple as this:

public interface IEncryptor
{
    public string Encrypt(object o);
    public string Decrypt(string o);
}

public class Encryptor : IEncryptor
{
    private readonly string _key;
    private readonly string _iv;

    public Encryption(IConfiguration configuration)
    {
        var section = configuration.GetSection("Encryption");
        _key = section.GetValue<string>("Key");
        _iv = section.GetValue<string>("IV");
    }
    public string Encrypt(object data)
    {
        return EncryptProvider.AESEncrypt(data.ToString(), _key, _iv);
    }

    public string Decrypt(string data)
    {
        return EncryptProvider.AESDecrypt(data, _key, _iv);
    }
}

I skip different checks for the sake of brevity and leave only the essential stuff, but it’s important to foresee different scenarios.

We’ll store the key and the initialization vector needed for AES encryption to work in the appsettings.json file, but they can reside anywhere else.

{
  "Encryption": {
    "Key": "U7KzmRT1Y69VkbqK7zLf7m64aPaXiS9m",
    "IV": "2P2AEIuWfStFT7Yy"
  }
}

The only thing left is to register this service as a singleton and we can use its interface to encrypt and decrypt data.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IEncryptor, Encryptor>();
}

In this case, we will be able to use this service anywhere but still have to call it manually. There is yet an opportunity to automate conversion, for which we need to use JSON converters.

JSON converter

In ASP.NET Core we can create custom JSON converters with defined logic for reading and writing values in a specific format.

  • We want our Id<Guid> be converted into an encrypted string while serializing.
  • And we need to deserialize an encrypted string, decrypt it and save the value in Id<Guid> object.
public class GuidConverter : JsonConverter<Id<Guid>>
{
    private readonly IEncryptor _encryptor;

    public Converter(IEncryptor encryptor)
    {
        _encryptor = encryptor;
    }

    public override Id<Guid> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        object decryptedGuid = _encryptor.Decrypt(reader.GetString());
        Guid.TryParse(decryptedGuid.ToString(), out Guid value);
        return new Id<Guid>(value);
    }

    public override void Write(Utf8JsonWriter writer, Id<Guid> value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(_encryptor.Encrypt(value.Value));
    }
}

Microsoft suggests using factories to extend JSON converters but for simplicity let’s use a converter directly.

In a Read method, we get a string value from the reader, decrypt it, convert it to Guid and return it in the form of an Id<Guid> object. You can imagine how many issues can happen in this step. So it makes sense to make this method generic and add different checks.

It’s even simpler inside a Write method: encrypt a value and write it into a string. And that’s it.

Next step, we need to register that converter:

public void ConfigureServices(IServiceCollection services)
{
    var encryptor = new Encryptor(Configuration);
    services.AddControllers().AddJsonOptions(x => 
        x.JsonSerializerOptions.Converters.Add(new Converter(encryptor)));
}

And now it’ll automatically handle serializing an Id<Guid> object into an encrypted string and create an Id<Guid> object from an encrypted string.

Note:

There is one more point for creating your implementation of encryption, since, for example, the chosen library generates a string in base64 format, which includes symbols like “” and “+”. It means that after encryption, you also need to encode the output. It will make it usable not only in the body of the response but also as a part of the route or a query parameter.

And if you make encryption yourself, it’s quite easy to convert encrypted bytes into the url-safe format of base64, where “/” and “+” are substituted by “-” and “_”.

IDOR prevention in action

The last step is to see how it all works in practice. For example, we have this basic controller.

[Route("api/[controller]")]
[ApiController]
public class EntitiesController : ControllerBase
{
    private readonly Entity _entity = new(new Id<Guid>(Guid.NewGuid()), "Entity");

    [HttpGet]
    public Entity Get() => _entity;

    [HttpPost]
    public Guid Post([FromBody] CreateEntity request) => request.Id.Value;
}

public class CreateEntity
{
    public Id<Guid> Id { get; set; }
    public string Name { get; set; }
}

Let’s make a request GET /api/entities. It should return an instance of an Entity class that is part of the controller. We generate an id automatically in the application, but in a response, it should be encrypted. And here’s the response:

{
  "id": "vqvKf2FDGuZ/X7YfQd5zb2oqY6TeKggN9kGNUh2/x38mHqRSl5str25XKGX9qfTl",
  "name": "Entity"
}

As you see, it’s impossible even to predict what real value is hidden beneath the cipher so we can sleep well.

Finally, let’s make a request POST /api/entities that should return an encrypted Guid value. We’ll use the response from the previous request as a body. The response of the POST endpoint:

"ca51d067-11e2-4ccc-a0d0-7ac190b994c9"

Conclusion

Although this implementation of indirect object reference prevention implies some far-reaching features of the application, such as an obligation to use some specific class for identifiers, it still leaves a lot of stuff under the hood, so we shouldn’t deal with encryption or decryption manually.

You can extend and improve this example in multiple ways:

  • Store an encrypted version of the id inside Id<T> class to always know in what form it’ll be returned to the client
  • Implement your encryptor to change the size of the encrypted value or the padding mode (ISO10126, for example, will generate a unique ending each time on encryption)
  • Create JSON converter factory sticking to best practices suggested by Microsoft
  • Or maybe something else

This foundation was meant to keep simple, but it serves one purpose: to show how to implement IDOR prevention in ASP.NET Core application.

Source code: https://github.com/Jyuart/IDORP

The post IDOR prevention in ASP.NET Core appeared first on Offshore Custom Software Development Company | Binary Studio, Ukraine.

Source: Binary Studio